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