001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2005-2005 Julian Hyde
008// Copyright (C) 2005-2013 Pentaho
009// All Rights Reserved.
010*/
011package mondrian.rolap;
012
013import mondrian.mdx.*;
014import mondrian.olap.*;
015import mondrian.olap.fun.AggregateFunDef;
016import mondrian.olap.fun.SetFunDef;
017import mondrian.resource.MondrianResource;
018import mondrian.rolap.agg.*;
019import mondrian.server.*;
020import mondrian.server.monitor.SqlStatementEvent;
021import mondrian.spi.Dialect;
022
023import org.apache.log4j.Logger;
024
025import org.olap4j.AllocationPolicy;
026import org.olap4j.Scenario;
027
028import java.sql.ResultSet;
029import java.sql.SQLException;
030import java.util.*;
031
032/**
033 * <code>RolapCell</code> implements {@link mondrian.olap.Cell} within a
034 * {@link RolapResult}.
035 */
036public class RolapCell implements Cell {
037    /**
038     * @see mondrian.util.Bug#olap4jUpgrade Use
039     * {@link mondrian.xmla.XmlaConstants}.ActionType.DRILLTHROUGH when present
040     */
041    private static final int MDACTION_TYPE_DRILLTHROUGH = 0x100;
042
043    private final RolapResult result;
044    protected final int[] pos;
045    protected RolapResult.CellInfo ci;
046
047    /**
048     * Creates a RolapCell.
049     *
050     * @param result Result cell belongs to
051     * @param pos Coordinates of cell
052     * @param ci Cell information, containing value et cetera
053     */
054    RolapCell(RolapResult result, int[] pos, RolapResult.CellInfo ci) {
055        this.result = result;
056        this.pos = pos;
057        this.ci = ci;
058    }
059
060    public List<Integer> getCoordinateList() {
061        return new AbstractList<Integer>() {
062            public Integer get(int index) {
063                return pos[index];
064            }
065
066            public int size() {
067                return pos.length;
068            }
069        };
070    }
071
072    public Object getValue() {
073        if (ci.value == Util.nullValue) {
074            return null;
075        }
076        return ci.value;
077    }
078
079    public String getCachedFormatString() {
080        return ci.formatString;
081    }
082
083    public String getFormattedValue() {
084        return ci.getFormatValue();
085    }
086
087    public boolean isNull() {
088        return (ci.value == Util.nullValue);
089    }
090
091    public boolean isError() {
092        return (ci.value instanceof Throwable);
093    }
094
095    public String getDrillThroughSQL(
096        boolean extendedContext)
097    {
098        return getDrillThroughSQL(
099            new ArrayList<Exp>(), extendedContext);
100    }
101
102    public String getDrillThroughSQL(
103        List<Exp> fields,
104        boolean extendedContext)
105    {
106        if (!MondrianProperties.instance()
107            .EnableDrillThrough.get())
108        {
109            throw MondrianResource.instance()
110                .DrillthroughDisabled.ex(
111                    MondrianProperties.instance()
112                        .EnableDrillThrough.getPath());
113        }
114        final Member[] currentMembers = getMembersForDrillThrough();
115        // Create a StarPredicate to represent the compound slicer
116        // (if necessary)
117        // NOTE: the method buildDrillthroughSlicerPredicate modifies
118        // the array of members, so it MUST be called before calling
119        // RolapAggregationManager.makeDrillThroughRequest
120        StarPredicate starPredicateSlicer =
121            buildDrillthroughSlicerPredicate(
122                currentMembers,
123                result.getSlicerAxis());
124        DrillThroughCellRequest cellRequest =
125            RolapAggregationManager.makeDrillThroughRequest(
126                currentMembers, extendedContext, result.getCube(),
127                fields);
128        if (cellRequest == null) {
129            return null;
130        }
131        final RolapConnection connection =
132            result.getExecution().getMondrianStatement()
133                .getMondrianConnection();
134        final RolapAggregationManager aggMgr =
135            connection.getServer().getAggregationManager();
136        return aggMgr.getDrillThroughSql(
137            cellRequest,
138            starPredicateSlicer,
139            fields,
140            false);
141    }
142
143    public int getDrillThroughCount() {
144        final Member[] currentMembers = getMembersForDrillThrough();
145        // Create a StarPredicate to represent the compound
146        // slicer (if necessary)
147        // NOTE: the method buildDrillthroughSlicerPredicate modifies
148        // the array of members, so it MUST be called before calling
149        // RolapAggregationManager.makeDrillThroughRequest
150        StarPredicate starPredicateSlicer =
151            buildDrillthroughSlicerPredicate(
152                currentMembers,
153                result.getSlicerAxis());
154        DrillThroughCellRequest cellRequest =
155            RolapAggregationManager.makeDrillThroughRequest(
156                currentMembers, false, result.getCube(), null);
157        if (cellRequest == null) {
158            return -1;
159        }
160        final RolapConnection connection =
161            result.getExecution().getMondrianStatement()
162                .getMondrianConnection();
163        final RolapAggregationManager aggMgr =
164            connection.getServer().getAggregationManager();
165        final String sql =
166            aggMgr.getDrillThroughSql(
167                cellRequest,
168                starPredicateSlicer,
169                new ArrayList<Exp>(),
170                true);
171
172        final SqlStatement stmt =
173            RolapUtil.executeQuery(
174                connection.getDataSource(),
175                sql,
176                new Locus(
177                    new Execution(connection.getInternalStatement(), 0),
178                    "RolapCell.getDrillThroughCount",
179                    "Error while counting drill-through"));
180        try {
181            ResultSet rs = stmt.getResultSet();
182            assert rs.getMetaData().getColumnCount() == 1;
183            rs.next();
184            ++stmt.rowCount;
185            return rs.getInt(1);
186        } catch (SQLException e) {
187            throw stmt.handle(e);
188        } finally {
189            stmt.close();
190        }
191    }
192
193    /**
194     * This method handles the case of a compound slicer with more than one
195     * {@link Position}. In this case, a simple array of {@link Member}s is not
196     * sufficient to express the set of drill through rows. If the slicer axis
197     * does have multiple positions, this method will do two things:
198     * <ol>
199     *  <li>Modify the passed-in array if any Member is overly restrictive.
200     *  This can happen if the slicer specifies multiple members in the same
201     *  hierarchy. In this scenario, the array of Members will contain an
202     *  element for only the last selected member in the hierarchy. This method
203     *  will replace that Member with the "All" Member from that hierarchy.
204     *  </li>
205     *  <li>Create a {@link StarPredicate} representing the Positions indicated
206     *  by the slicer axis.
207     *  </li>
208     * </ol>
209     *
210     * @param membersForDrillthrough the array of Members returned by
211     * {@link #getMembersForDrillThrough()}
212     * @param slicerAxis the slicer {@link Axis}
213     * @return an instance of <code>StarPredicate</code> representing all
214     * of the the positions from the slicer if it has more than one,
215     * or <code>null</code> otherwise.
216     */
217    private StarPredicate buildDrillthroughSlicerPredicate(
218        Member[] membersForDrillthrough,
219        Axis slicerAxis)
220    {
221        List<Position> listOfPositions = slicerAxis.getPositions();
222        // If the slicer has zero or one position(s),
223        // then there is no need to do
224        // anything; the array of Members is correct as-is
225        if (listOfPositions.size() <= 1) {
226            return null;
227        }
228        // First, iterate through the positions' members, un-constraining the
229        // "membersForDrillthrough" array if any position member is not
230        // in the array
231        for (Position position : listOfPositions) {
232            for (Member member : position) {
233                RolapHierarchy rolapHierarchy =
234                    (RolapHierarchy) member.getHierarchy();
235                // Check if the membersForDrillthrough constraint is identical
236                // to that of the position member
237                if (!membersForDrillthrough[rolapHierarchy.getOrdinalInCube()]
238                    .equals(member))
239                {
240                    // There is a discrepancy, so un-constrain the
241                    // membersForDrillthrough array
242                    membersForDrillthrough[rolapHierarchy.getOrdinalInCube()] =
243                        rolapHierarchy.getAllMember();
244                }
245            }
246        }
247        // This is a list containing an AndPredicate for each position in the
248        // slicer axis
249        List<StarPredicate> listOfStarPredicatesForSlicerPositions =
250            new ArrayList<StarPredicate>();
251        // Now we re-iterate the positions' members,
252        // creating the slicer constraint
253        for (Position position : listOfPositions) {
254            // This is a list of the predicates required to select the
255            // current position (excluding the members of the position
256            // that are already constrained in the membersForDrillthrough array)
257            List<StarPredicate> listOfStarPredicatesForCurrentPosition =
258                new ArrayList<StarPredicate>();
259            // Iterate the members of the current position
260            for (Member member : position) {
261                RolapHierarchy rolapHierarchy =
262                    (RolapHierarchy) member.getHierarchy();
263                // If the membersForDrillthrough is already constraining to
264                // this member, then there is no need to create additional
265                // predicate(s) for this member
266                if (!membersForDrillthrough[rolapHierarchy.getOrdinalInCube()]
267                   .equals(member))
268                {
269                    // Walk up the member's hierarchy, adding a
270                    // predicate for each level
271                    Member memberWalk = member;
272                    Level levelLast = null;
273                    while (memberWalk != null && ! memberWalk.isAll()) {
274                        // Only create a predicate for this member if we
275                        // are at a new level. This is for parent-child levels,
276                        // however it still suffers from the following bug:
277                        //  http://jira.pentaho.com/browse/MONDRIAN-318
278                        if (memberWalk.getLevel() != levelLast) {
279                            RolapCubeMember rolapCubeMember =
280                                (RolapCubeMember) memberWalk;
281                            RolapStar.Column column =
282                                rolapCubeMember.getLevel()
283                                    .getBaseStarKeyColumn(result.getCube());
284                            // Add a predicate for the member at this level
285                            listOfStarPredicatesForCurrentPosition.add(
286                                new MemberColumnPredicate(
287                                    column,
288                                    rolapCubeMember));
289                        }
290                        levelLast = memberWalk.getLevel();
291                        // Walk up the hierarchy
292                        memberWalk = memberWalk.getParentMember();
293                    }
294                }
295            }
296            // AND together all of the predicates that specify
297            // the current position
298            StarPredicate starPredicateForCurrentSlicerPosition =
299                new AndPredicate(listOfStarPredicatesForCurrentPosition);
300            // Add this position's predicate to the list
301            listOfStarPredicatesForSlicerPositions
302                .add(starPredicateForCurrentSlicerPosition);
303        }
304        // OR together the predicates for all of the slicer's
305        // positions and return
306        return new OrPredicate(listOfStarPredicatesForSlicerPositions);
307    }
308
309    /**
310     * Returns whether it is possible to drill through this cell.
311     * Drill-through is possible if the measure is a stored measure
312     * and not possible for calculated measures.
313     *
314     * @return true if can drill through
315     */
316    public boolean canDrillThrough() {
317        if (!MondrianProperties.instance()
318            .EnableDrillThrough.get())
319        {
320            return false;
321        }
322        // get current members
323        final Member[] currentMembers = getMembersForDrillThrough();
324        if (containsCalcMembers(currentMembers)) {
325            return false;
326        }
327        Cube x = chooseDrillThroughCube(currentMembers, result.getCube());
328        return x != null;
329    }
330
331    private boolean containsCalcMembers(Member[] currentMembers) {
332        // Any calculated members which are not measures, we can't drill
333        // through. Trivial calculated members should have been converted
334        // already. We allow simple calculated measures such as
335        // [Measures].[Unit Sales] / [Measures].[Store Sales] provided that both
336        // are from the same cube.
337        for (int i = 1; i < currentMembers.length; i++) {
338            final Member currentMember = currentMembers[i];
339            if (currentMember.isCalculated()) {
340                return true;
341            }
342        }
343        return false;
344    }
345
346    public static RolapCube chooseDrillThroughCube(
347        Member[] currentMembers,
348        RolapCube defaultCube)
349    {
350        if (defaultCube != null && defaultCube.isVirtual()) {
351            List<RolapCube> cubes = new ArrayList<RolapCube>();
352            for (RolapMember member : defaultCube.getMeasuresMembers()) {
353                if (member instanceof RolapVirtualCubeMeasure) {
354                    RolapVirtualCubeMeasure measure =
355                        (RolapVirtualCubeMeasure) member;
356                    cubes.add(measure.getCube());
357                }
358            }
359            defaultCube = cubes.get(0);
360            assert !defaultCube.isVirtual();
361        }
362        final DrillThroughVisitor visitor =
363            new DrillThroughVisitor();
364        try {
365            for (Member member : currentMembers) {
366                visitor.handleMember(member);
367            }
368        } catch (RuntimeException e) {
369            if (e == DrillThroughVisitor.bomb) {
370                // No cubes left
371                return null;
372            } else {
373                throw e;
374            }
375        }
376        return visitor.cube == null
377             ? defaultCube
378             : visitor.cube;
379    }
380
381    private Member[] getMembersForDrillThrough() {
382        final Member[] currentMembers = result.getCellMembers(pos);
383
384        // replace member if we're dealing with a trivial formula
385        List<Member> memberList = Arrays.asList(currentMembers);
386        for (int i = 0; i < currentMembers.length; i++) {
387            replaceTrivialCalcMember(i, memberList);
388        }
389        return currentMembers;
390    }
391
392    private void replaceTrivialCalcMember(int i, List<Member> members) {
393        Member member = members.get(i);
394        if (!member.isCalculated()) {
395            return;
396        }
397        member = RolapUtil.strip((RolapMember) member);
398        // if "cm" is a calc member defined by
399        // "with member cm as m" then
400        // "cm" is equivalent to "m"
401        final Exp expr = member.getExpression();
402        if (expr instanceof MemberExpr) {
403            members.set(
404                i,
405                ((MemberExpr) expr).getMember());
406            return;
407        }
408        // "Aggregate({m})" is equivalent to "m"
409        if (expr instanceof ResolvedFunCall) {
410            ResolvedFunCall call = (ResolvedFunCall) expr;
411            if (call.getFunDef() instanceof AggregateFunDef) {
412                final Exp[] args = call.getArgs();
413                if (args[0] instanceof ResolvedFunCall) {
414                    final ResolvedFunCall arg0 = (ResolvedFunCall) args[0];
415                    if (arg0.getFunDef() instanceof SetFunDef) {
416                        if (arg0.getArgCount() == 1
417                            && arg0.getArg(0) instanceof MemberExpr)
418                        {
419                            final MemberExpr memberExpr =
420                                (MemberExpr) arg0.getArg(0);
421                            members.set(i, memberExpr.getMember());
422                        }
423                    }
424                }
425            }
426        }
427    }
428
429    /**
430     * Generates an executes a SQL statement to drill through this cell.
431     *
432     * <p>Throws if this cell is not drillable.
433     *
434     * <p>Enforces limits on the starting and last row.
435     *
436     * <p>If tabFields is not null, returns the specified columns. (This option
437     * is deprecated.)
438     *
439     * @param maxRowCount Maximum number of rows to retrieve, <= 0 if unlimited
440     * @param firstRowOrdinal Ordinal of row to skip to (1-based), or 0 to
441     *   start from beginning
442     * @param fields            List of field expressions to return as the
443     *                          result set columns.
444     * @param extendedContext   If true, add non-constraining columns to the
445     *                          query for levels below each current member.
446     *                          This additional context makes the drill-through
447     *                          queries easier for humans to understand.
448     * @param logger Logger. If not null and debug is enabled, log SQL here
449     * @return executed SQL statement
450     */
451    public SqlStatement drillThroughInternal(
452        int maxRowCount,
453        int firstRowOrdinal,
454        List<Exp> fields,
455        boolean extendedContext,
456        Logger logger)
457    {
458        if (!canDrillThrough()) {
459            throw Util.newError("Cannot do DrillThrough operation on the cell");
460        }
461
462        // Generate SQL.
463        String sql = getDrillThroughSQL(fields, extendedContext);
464        if (logger != null && logger.isDebugEnabled()) {
465            logger.debug("drill through sql: " + sql);
466        }
467
468        // Choose the appropriate scrollability. If we need to start from an
469        // offset row, it is useful that the cursor is scrollable, but not
470        // essential.
471        final Statement statement =
472            result.getExecution().getMondrianStatement();
473        final Execution execution = new Execution(statement, 0);
474        final Connection connection = statement.getMondrianConnection();
475        int resultSetType = ResultSet.TYPE_SCROLL_INSENSITIVE;
476        int resultSetConcurrency = ResultSet.CONCUR_READ_ONLY;
477        final Schema schema = statement.getSchema();
478        Dialect dialect = ((RolapSchema) schema).getDialect();
479        if (!dialect.supportsResultSetConcurrency(
480                resultSetType, resultSetConcurrency)
481            || firstRowOrdinal <= 1)
482        {
483            // downgrade to non-scroll cursor, since we can
484            // fake absolute() via forward fetch
485            resultSetType = ResultSet.TYPE_FORWARD_ONLY;
486        }
487        return
488            RolapUtil.executeQuery(
489                connection.getDataSource(),
490                sql,
491                null,
492                maxRowCount,
493                firstRowOrdinal,
494                new SqlStatement.StatementLocus(
495                    execution,
496                    "RolapCell.drillThrough",
497                    "Error in drill through",
498                    SqlStatementEvent.Purpose.DRILL_THROUGH, 0),
499                resultSetType,
500                resultSetConcurrency,
501                null);
502    }
503
504    public Object getPropertyValue(String propertyName) {
505        final boolean matchCase =
506            MondrianProperties.instance().CaseSensitive.get();
507        Property property = Property.lookup(propertyName, matchCase);
508        Object defaultValue = null;
509        if (property != null) {
510            switch (property.ordinal) {
511            case Property.CELL_ORDINAL_ORDINAL:
512                return result.getCellOrdinal(pos);
513            case Property.VALUE_ORDINAL:
514                return getValue();
515            case Property.FORMAT_STRING_ORDINAL:
516                if (ci.formatString == null) {
517                    final Evaluator evaluator = result.getRootEvaluator();
518                    final int savepoint = evaluator.savepoint();
519                    try {
520                        result.populateEvaluator(evaluator, pos);
521                        ci.formatString = evaluator.getFormatString();
522                    } finally {
523                        evaluator.restore(savepoint);
524                    }
525                }
526                return ci.formatString;
527            case Property.FORMATTED_VALUE_ORDINAL:
528                return getFormattedValue();
529            case Property.FONT_FLAGS_ORDINAL:
530                defaultValue = 0;
531                break;
532            case Property.SOLVE_ORDER_ORDINAL:
533                defaultValue = 0;
534                break;
535            case Property.ACTION_TYPE_ORDINAL:
536                return canDrillThrough() ? MDACTION_TYPE_DRILLTHROUGH : 0;
537            case Property.DRILLTHROUGH_COUNT_ORDINAL:
538                return canDrillThrough() ? getDrillThroughCount() : -1;
539            default:
540                // fall through
541            }
542        }
543        final Evaluator evaluator = result.getRootEvaluator();
544        final int savepoint = evaluator.savepoint();
545        try {
546            result.populateEvaluator(evaluator, pos);
547            return evaluator.getProperty(propertyName, defaultValue);
548        } finally {
549            evaluator.restore(savepoint);
550        }
551    }
552
553    public Member getContextMember(Hierarchy hierarchy) {
554        return result.getMember(pos, hierarchy);
555    }
556
557    public void setValue(
558        Scenario scenario,
559        Object newValue,
560        AllocationPolicy allocationPolicy,
561        Object... allocationArgs)
562    {
563        if (allocationPolicy == null) {
564            // user error
565            throw Util.newError(
566                "Allocation policy must not be null");
567        }
568        final RolapMember[] members = result.getCellMembers(pos);
569        for (int i = 0; i < members.length; i++) {
570            Member member = members[i];
571            if (ScenarioImpl.isScenario(member.getHierarchy())) {
572                scenario =
573                    (Scenario) member.getPropertyValue(Property.SCENARIO.name);
574                members[i] = (RolapMember) member.getHierarchy().getAllMember();
575            } else if (member.isCalculated()) {
576                throw Util.newError(
577                    "Cannot write to cell: one of the coordinates ("
578                    + member.getUniqueName()
579                    + ") is a calculated member");
580            }
581        }
582        if (scenario == null) {
583            throw Util.newError("No active scenario");
584        }
585        if (allocationArgs == null) {
586            allocationArgs = new Object[0];
587        }
588        final Object currentValue = getValue();
589        double doubleCurrentValue;
590        if (currentValue == null) {
591            doubleCurrentValue = 0d;
592        } else if (currentValue instanceof Number) {
593            doubleCurrentValue = ((Number) currentValue).doubleValue();
594        } else {
595            // Cell is not a number. Likely it is a string or a
596            // MondrianEvaluationException. Do not attempt to change the value
597            // in this case. (REVIEW: Is this the correct behavior?)
598            return;
599        }
600        double doubleNewValue = ((Number) newValue).doubleValue();
601        ((ScenarioImpl) scenario).setCellValue(
602            result.getExecution().getMondrianStatement()
603                .getMondrianConnection(),
604            Arrays.asList(members),
605            doubleNewValue,
606            doubleCurrentValue,
607            allocationPolicy,
608            allocationArgs);
609    }
610
611    /**
612     * Visitor that walks over a cell's expression and checks whether the
613     * cell should allow drill-through. If not, throws the {@link #bomb}
614     * exception.
615     *
616     * <p>Examples:</p>
617     * <ul>
618     * <li>Literal 1 is drillable</li>
619     * <li>Member [Measures].[Unit Sales] is drillable</li>
620     * <li>Calculated member with expression [Measures].[Unit Sales] +
621     *     1 is drillable</li>
622     * <li>Calculated member with expression
623     *     ([Measures].[Unit Sales], [Time].PrevMember) is not drillable</li>
624     * </ul>
625     */
626    private static class DrillThroughVisitor extends MdxVisitorImpl {
627        static final RuntimeException bomb = new RuntimeException();
628        RolapCube cube = null;
629
630        DrillThroughVisitor() {
631        }
632
633        public Object visit(MemberExpr memberExpr) {
634            handleMember(memberExpr.getMember());
635            return null;
636        }
637
638        public Object visit(ResolvedFunCall call) {
639            final FunDef def = call.getFunDef();
640            final Exp[] args = call.getArgs();
641            final String name = def.getName();
642            if (name.equals("+")
643                || name.equals("-")
644                || name.equals("/")
645                || name.equals("*")
646                || name.equals("CoalesceEmpty")
647                // Allow parentheses but don't allow tuple
648                || name.equals("()") && args.length == 1)
649            {
650                return null;
651            }
652            throw bomb;
653        }
654
655        public void handleMember(Member member) {
656            if (member instanceof RolapStoredMeasure) {
657                // If this member is in a different cube that previous members
658                // we've seen, we cannot drill through.
659                final RolapCube cube = ((RolapStoredMeasure) member).getCube();
660                if (this.cube == null) {
661                    this.cube = cube;
662                } else if (this.cube != cube) {
663                    // this measure lives in a different cube than previous
664                    // measures we have seen
665                    throw bomb;
666                }
667            } else if (member instanceof RolapCubeMember) {
668                handleMember(((RolapCubeMember) member).member);
669            } else if (member
670                instanceof RolapHierarchy.RolapCalculatedMeasure)
671            {
672                RolapHierarchy.RolapCalculatedMeasure measure =
673                    (RolapHierarchy.RolapCalculatedMeasure) member;
674                measure.getFormula().getExpression().accept(this);
675            } else if (member instanceof RolapMember) {
676                // regular RolapMember - fine
677            } else {
678                // don't know what this is!
679                throw bomb;
680            }
681        }
682
683        public Object visit(NamedSetExpr namedSetExpr) {
684            throw Util.newInternal("not valid here: " + namedSetExpr);
685        }
686
687        public Object visit(Literal literal) {
688            return null; // literals are drillable
689        }
690
691        public Object visit(Query query) {
692            throw Util.newInternal("not valid here: " + query);
693        }
694
695        public Object visit(QueryAxis queryAxis) {
696            throw Util.newInternal("not valid here: " + queryAxis);
697        }
698
699        public Object visit(Formula formula) {
700            throw Util.newInternal("not valid here: " + formula);
701        }
702
703        public Object visit(UnresolvedFunCall call) {
704            throw Util.newInternal("expected resolved expression");
705        }
706
707        public Object visit(Id id) {
708            throw Util.newInternal("expected resolved expression");
709        }
710
711        public Object visit(ParameterExpr parameterExpr) {
712            // Not valid in general; might contain complex expression
713            throw bomb;
714        }
715
716        public Object visit(DimensionExpr dimensionExpr) {
717            // Not valid in general; might be part of complex expression
718            throw bomb;
719        }
720
721        public Object visit(HierarchyExpr hierarchyExpr) {
722            // Not valid in general; might be part of complex expression
723            throw bomb;
724        }
725
726        public Object visit(LevelExpr levelExpr) {
727            // Not valid in general; might be part of complex expression
728            throw bomb;
729        }
730    }
731}
732
733// End RolapCell.java