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) 2001-2005 Julian Hyde 008// Copyright (C) 2005-2012 Pentaho and others 009// All Rights Reserved. 010*/ 011package mondrian.rolap; 012 013import mondrian.calc.*; 014import mondrian.calc.impl.*; 015import mondrian.mdx.*; 016import mondrian.olap.*; 017import mondrian.olap.DimensionType; 018import mondrian.olap.LevelType; 019import mondrian.olap.Role.HierarchyAccess; 020import mondrian.olap.fun.*; 021import mondrian.olap.type.*; 022import mondrian.resource.MondrianResource; 023import mondrian.rolap.RestrictedMemberReader.MultiCardinalityDefaultMember; 024import mondrian.rolap.sql.MemberChildrenConstraint; 025import mondrian.rolap.sql.SqlQuery; 026import mondrian.spi.CellFormatter; 027import mondrian.spi.impl.Scripts; 028import mondrian.util.UnionIterator; 029 030import org.apache.log4j.Logger; 031 032import java.io.PrintWriter; 033import java.util.*; 034 035/** 036 * <code>RolapHierarchy</code> implements {@link Hierarchy} for a ROLAP database. 037 * 038 * <p>The ordinal of a hierarchy <em>within a particular cube</em> is found by 039 * calling {@link #getOrdinalInCube()}. Ordinals are contiguous and zero-based. 040 * Zero is always the <code>[Measures]</code> dimension. 041 * 042 * <p>NOTE: It is only valid to call that method on the measures hierarchy, and 043 * on members of the {@link RolapCubeHierarchy} subclass. When the measures 044 * hierarchy is of that class, we will move the method down.) 045 * 046 * @author jhyde 047 * @since 10 August, 2001 048 */ 049public class RolapHierarchy extends HierarchyBase { 050 051 private static final Logger LOGGER = Logger.getLogger(RolapHierarchy.class); 052 053 /** 054 * The raw member reader. For a member reader which incorporates access 055 * control and deals with hidden members (if the hierarchy is ragged), use 056 * {@link #createMemberReader(Role)}. 057 */ 058 private MemberReader memberReader; 059 protected MondrianDef.Hierarchy xmlHierarchy; 060 private String memberReaderClass; 061 protected MondrianDef.RelationOrJoin relation; 062 private Member defaultMember; 063 private String defaultMemberName; 064 private RolapNullMember nullMember; 065 066 private String sharedHierarchyName; 067 private String uniqueKeyLevelName; 068 069 private Exp aggregateChildrenExpression; 070 071 /** 072 * The level that the null member belongs too. 073 */ 074 protected final RolapLevel nullLevel; 075 076 /** 077 * The 'all' member of this hierarchy. This exists even if the hierarchy 078 * does not officially have an 'all' member. 079 */ 080 private RolapMemberBase allMember; 081 private static final String ALL_LEVEL_CARDINALITY = "1"; 082 private final Map<String, Annotation> annotationMap; 083 final RolapHierarchy closureFor; 084 085 /** 086 * Creates a hierarchy. 087 * 088 * @param dimension Dimension 089 * @param subName Name of this hierarchy 090 * @param hasAll Whether hierarchy has an 'all' member 091 * @param closureFor Hierarchy for which the new hierarchy is a closure; 092 * null for regular hierarchies 093 */ 094 RolapHierarchy( 095 RolapDimension dimension, 096 String subName, 097 String caption, 098 boolean visible, 099 String description, 100 boolean hasAll, 101 RolapHierarchy closureFor, 102 Map<String, Annotation> annotationMap) 103 { 104 super(dimension, subName, caption, visible, description, hasAll); 105 this.annotationMap = annotationMap; 106 this.allLevelName = "(All)"; 107 this.allMemberName = 108 subName != null 109 && (MondrianProperties.instance().SsasCompatibleNaming.get() 110 || name.equals(subName + "." + subName)) 111 ? "All " + subName + "s" 112 : "All " + name + "s"; 113 this.closureFor = closureFor; 114 if (hasAll) { 115 this.levels = new RolapLevel[1]; 116 this.levels[0] = 117 new RolapLevel( 118 this, 119 this.allLevelName, 120 null, 121 true, 122 null, 123 0, 124 null, 125 null, 126 null, 127 null, 128 null, 129 null, 130 null, 131 RolapProperty.emptyArray, 132 RolapLevel.FLAG_ALL | RolapLevel.FLAG_UNIQUE, 133 null, 134 null, 135 RolapLevel.HideMemberCondition.Never, 136 LevelType.Regular, 137 "", 138 Collections.<String, Annotation>emptyMap()); 139 } else { 140 this.levels = new RolapLevel[0]; 141 } 142 143 // The null member belongs to a level with very similar properties to 144 // the 'all' level. 145 this.nullLevel = 146 new RolapLevel( 147 this, 148 this.allLevelName, 149 null, 150 true, 151 null, 152 0, 153 null, 154 null, 155 null, 156 null, 157 null, 158 null, 159 null, 160 RolapProperty.emptyArray, 161 RolapLevel.FLAG_ALL | RolapLevel.FLAG_UNIQUE, 162 null, 163 null, 164 RolapLevel.HideMemberCondition.Never, 165 LevelType.Null, 166 "", 167 Collections.<String, Annotation>emptyMap()); 168 } 169 170 /** 171 * Creates a <code>RolapHierarchy</code>. 172 * 173 * @param dimension the dimension this hierarchy belongs to 174 * @param xmlHierarchy the xml object defining this hierarchy 175 * @param xmlCubeDimension the xml object defining the cube 176 * dimension for this object 177 */ 178 RolapHierarchy( 179 RolapDimension dimension, 180 MondrianDef.Hierarchy xmlHierarchy, 181 MondrianDef.CubeDimension xmlCubeDimension) 182 { 183 this( 184 dimension, 185 xmlHierarchy.name, 186 xmlHierarchy.caption, 187 xmlHierarchy.visible, 188 xmlHierarchy.description, 189 xmlHierarchy.hasAll, 190 null, 191 createAnnotationMap(xmlHierarchy.annotations)); 192 193 assert !(this instanceof RolapCubeHierarchy); 194 195 this.xmlHierarchy = xmlHierarchy; 196 this.relation = xmlHierarchy.relation; 197 if (xmlHierarchy.relation instanceof MondrianDef.InlineTable) { 198 this.relation = 199 RolapUtil.convertInlineTableToRelation( 200 (MondrianDef.InlineTable) xmlHierarchy.relation, 201 getRolapSchema().getDialect()); 202 } 203 this.memberReaderClass = xmlHierarchy.memberReaderClass; 204 this.uniqueKeyLevelName = xmlHierarchy.uniqueKeyLevelName; 205 206 // Create an 'all' level even if the hierarchy does not officially 207 // have one. 208 if (xmlHierarchy.allMemberName != null) { 209 this.allMemberName = xmlHierarchy.allMemberName; 210 } 211 if (xmlHierarchy.allLevelName != null) { 212 this.allLevelName = xmlHierarchy.allLevelName; 213 } 214 RolapLevel allLevel = 215 new RolapLevel( 216 this, 217 this.allLevelName, 218 null, 219 true, 220 null, 221 0, 222 null, 223 null, 224 null, 225 null, 226 null, 227 null, 228 null, 229 RolapProperty.emptyArray, 230 RolapLevel.FLAG_ALL | RolapLevel.FLAG_UNIQUE, 231 null, 232 null, 233 RolapLevel.HideMemberCondition.Never, 234 LevelType.Regular, ALL_LEVEL_CARDINALITY, 235 Collections.<String, Annotation>emptyMap()); 236 allLevel.init(xmlCubeDimension); 237 this.allMember = new RolapMemberBase( 238 null, allLevel, RolapUtil.sqlNullValue, 239 allMemberName, Member.MemberType.ALL); 240 // assign "all member" caption 241 if (xmlHierarchy.allMemberCaption != null 242 && xmlHierarchy.allMemberCaption.length() > 0) 243 { 244 this.allMember.setCaption(xmlHierarchy.allMemberCaption); 245 } 246 this.allMember.setOrdinal(0); 247 248 if (xmlHierarchy.levels.length == 0) { 249 throw MondrianResource.instance().HierarchyHasNoLevels.ex( 250 getUniqueName()); 251 } 252 253 Set<String> levelNameSet = new HashSet<String>(); 254 for (MondrianDef.Level level : xmlHierarchy.levels) { 255 if (!levelNameSet.add(level.name)) { 256 throw MondrianResource.instance().HierarchyLevelNamesNotUnique 257 .ex( 258 getUniqueName(), level.name); 259 } 260 } 261 262 // If the hierarchy has an 'all' member, the 'all' level is level 0. 263 if (hasAll) { 264 this.levels = new RolapLevel[xmlHierarchy.levels.length + 1]; 265 this.levels[0] = allLevel; 266 for (int i = 0; i < xmlHierarchy.levels.length; i++) { 267 final MondrianDef.Level xmlLevel = xmlHierarchy.levels[i]; 268 if (xmlLevel.getKeyExp() == null 269 && xmlHierarchy.memberReaderClass == null) 270 { 271 throw MondrianResource.instance() 272 .LevelMustHaveNameExpression.ex(xmlLevel.name); 273 } 274 levels[i + 1] = new RolapLevel(this, i + 1, xmlLevel); 275 } 276 } else { 277 this.levels = new RolapLevel[xmlHierarchy.levels.length]; 278 for (int i = 0; i < xmlHierarchy.levels.length; i++) { 279 levels[i] = new RolapLevel(this, i, xmlHierarchy.levels[i]); 280 } 281 } 282 283 if (xmlCubeDimension instanceof MondrianDef.DimensionUsage) { 284 String sharedDimensionName = 285 ((MondrianDef.DimensionUsage) xmlCubeDimension).source; 286 this.sharedHierarchyName = sharedDimensionName; 287 if (subName != null) { 288 this.sharedHierarchyName += "." + subName; // e.g. "Time.Weekly" 289 } 290 } else { 291 this.sharedHierarchyName = null; 292 } 293 if (xmlHierarchy.relation != null 294 && xmlHierarchy.memberReaderClass != null) 295 { 296 throw MondrianResource.instance() 297 .HierarchyMustNotHaveMoreThanOneSource.ex(getUniqueName()); 298 } 299 if (!Util.isEmpty(xmlHierarchy.caption)) { 300 setCaption(xmlHierarchy.caption); 301 } 302 defaultMemberName = xmlHierarchy.defaultMember; 303 } 304 305 public static Map<String, Annotation> createAnnotationMap( 306 MondrianDef.Annotations annotations) 307 { 308 if (annotations == null 309 || annotations.array == null 310 || annotations.array.length == 0) 311 { 312 return Collections.emptyMap(); 313 } 314 // Use linked hash map because it retains order. 315 final Map<String, Annotation> map = 316 new LinkedHashMap<String, Annotation>(); 317 for (MondrianDef.Annotation annotation : annotations.array) { 318 final String name = annotation.name; 319 final String value = annotation.cdata; 320 map.put( 321 annotation.name, 322 new Annotation() { 323 public String getName() { 324 return name; 325 } 326 327 public Object getValue() { 328 return value; 329 } 330 }); 331 } 332 return map; 333 } 334 335 protected Logger getLogger() { 336 return LOGGER; 337 } 338 339 public boolean equals(Object o) { 340 if (this == o) { 341 return true; 342 } 343 if (!(o instanceof RolapHierarchy)) { 344 return false; 345 } 346 347 RolapHierarchy that = (RolapHierarchy)o; 348 if (sharedHierarchyName == null || that.sharedHierarchyName == null) { 349 return false; 350 } else { 351 return sharedHierarchyName.equals(that.sharedHierarchyName) 352 && getUniqueName().equals(that.getUniqueName()); 353 } 354 } 355 356 protected int computeHashCode() { 357 return super.computeHashCode() 358 ^ (sharedHierarchyName == null 359 ? 0 360 : sharedHierarchyName.hashCode()); 361 } 362 363 /** 364 * Initializes a hierarchy within the context of a cube. 365 */ 366 void init(MondrianDef.CubeDimension xmlDimension) { 367 // first create memberReader 368 if (this.memberReader == null) { 369 this.memberReader = getRolapSchema().createMemberReader( 370 sharedHierarchyName, this, memberReaderClass); 371 } 372 for (Level level : levels) { 373 ((RolapLevel) level).init(xmlDimension); 374 } 375 if (defaultMemberName != null) { 376 List<Id.Segment> uniqueNameParts; 377 if (defaultMemberName.contains("[")) { 378 uniqueNameParts = Util.parseIdentifier(defaultMemberName); 379 } else { 380 uniqueNameParts = 381 Collections.<Id.Segment>singletonList( 382 new Id.NameSegment( 383 defaultMemberName, 384 Id.Quoting.UNQUOTED)); 385 } 386 387 // First look up from within this hierarchy. Works for unqualified 388 // names, e.g. [USA].[CA]. 389 defaultMember = (Member) Util.lookupCompound( 390 getRolapSchema().getSchemaReader(), 391 this, 392 uniqueNameParts, 393 false, 394 Category.Member, 395 MatchType.EXACT); 396 397 // Next look up within global context. Works for qualified names, 398 // e.g. [Store].[USA].[CA] or [Time].[Weekly].[1997].[Q2]. 399 if (defaultMember == null) { 400 defaultMember = (Member) Util.lookupCompound( 401 getRolapSchema().getSchemaReader(), 402 new DummyElement(), 403 uniqueNameParts, 404 false, 405 Category.Member, 406 MatchType.EXACT); 407 } 408 if (defaultMember == null) { 409 throw Util.newInternal( 410 "Can not find Default Member with name \"" 411 + defaultMemberName + "\" in Hierarchy \"" 412 + getName() + "\""); 413 } 414 } 415 } 416 417 void setMemberReader(MemberReader memberReader) { 418 this.memberReader = memberReader; 419 } 420 421 MemberReader getMemberReader() { 422 return memberReader; 423 } 424 425 public Map<String, Annotation> getAnnotationMap() { 426 return annotationMap; 427 } 428 429 RolapLevel newMeasuresLevel() { 430 RolapLevel level = 431 new RolapLevel( 432 this, 433 "MeasuresLevel", 434 null, 435 true, 436 null, 437 this.levels.length, 438 null, 439 null, 440 null, 441 null, 442 null, 443 null, 444 null, 445 RolapProperty.emptyArray, 446 0, 447 null, 448 null, 449 RolapLevel.HideMemberCondition.Never, 450 LevelType.Regular, 451 "", 452 Collections.<String, Annotation>emptyMap()); 453 this.levels = Util.append(this.levels, level); 454 return level; 455 } 456 457 /** 458 * If this hierarchy has precisely one table, returns that table; 459 * if this hierarchy has no table, return the cube's fact-table; 460 * otherwise, returns null. 461 */ 462 MondrianDef.Relation getUniqueTable() { 463 if (relation instanceof MondrianDef.Relation) { 464 return (MondrianDef.Relation) relation; 465 } else if (relation instanceof MondrianDef.Join) { 466 return null; 467 } else { 468 throw Util.newInternal( 469 "hierarchy's relation is a " + relation.getClass()); 470 } 471 } 472 473 boolean tableExists(String tableName) { 474 return (relation != null) && getTable(tableName, relation) != null; 475 } 476 477 MondrianDef.Relation getTable(String tableName) { 478 return relation == null ? null : getTable(tableName, relation); 479 } 480 481 private static MondrianDef.Relation getTable( 482 String tableName, 483 MondrianDef.RelationOrJoin relationOrJoin) 484 { 485 if (relationOrJoin instanceof MondrianDef.Relation) { 486 MondrianDef.Relation relation = 487 (MondrianDef.Relation) relationOrJoin; 488 if (relation.getAlias().equals(tableName)) { 489 return relation; 490 } else { 491 return null; 492 } 493 } else { 494 MondrianDef.Join join = (MondrianDef.Join) relationOrJoin; 495 MondrianDef.Relation rel = getTable(tableName, join.left); 496 if (rel != null) { 497 return rel; 498 } 499 return getTable(tableName, join.right); 500 } 501 } 502 503 public RolapSchema getRolapSchema() { 504 return (RolapSchema) dimension.getSchema(); 505 } 506 507 public MondrianDef.RelationOrJoin getRelation() { 508 return relation; 509 } 510 511 public MondrianDef.Hierarchy getXmlHierarchy() { 512 return xmlHierarchy; 513 } 514 515 public Member getDefaultMember() { 516 // use lazy initialization to get around bootstrap issues 517 if (defaultMember == null) { 518 List<RolapMember> rootMembers = memberReader.getRootMembers(); 519 final SchemaReader schemaReader = 520 getRolapSchema().getSchemaReader(); 521 List<RolapMember> calcMemberList = 522 Util.cast(schemaReader.getCalculatedMembers(getLevels()[0])); 523 for (RolapMember rootMember 524 : UnionIterator.over(rootMembers, calcMemberList)) 525 { 526 if (rootMember.isHidden()) { 527 continue; 528 } 529 // Note: We require that the root member is not a hidden member 530 // of a ragged hierarchy, but we do not require that it is 531 // visible. In particular, if a cube contains no explicit 532 // measures, the default measure will be the implicitly defined 533 // [Fact Count] measure, which happens to be non-visible. 534 defaultMember = rootMember; 535 break; 536 } 537 if (defaultMember == null) { 538 throw MondrianResource.instance().InvalidHierarchyCondition.ex( 539 this.getUniqueName()); 540 } 541 } 542 return defaultMember; 543 } 544 545 public Member getNullMember() { 546 // use lazy initialization to get around bootstrap issues 547 if (nullMember == null) { 548 nullMember = new RolapNullMember(nullLevel); 549 } 550 return nullMember; 551 } 552 553 /** 554 * Returns the 'all' member. 555 */ 556 public RolapMember getAllMember() { 557 return allMember; 558 } 559 560 public Member createMember( 561 Member parent, 562 Level level, 563 String name, 564 Formula formula) 565 { 566 if (formula == null) { 567 return new RolapMemberBase( 568 (RolapMember) parent, (RolapLevel) level, name); 569 } else if (level.getDimension().isMeasures()) { 570 return new RolapCalculatedMeasure( 571 (RolapMember) parent, (RolapLevel) level, name, formula); 572 } else { 573 return new RolapCalculatedMember( 574 (RolapMember) parent, (RolapLevel) level, name, formula); 575 } 576 } 577 578 String getAlias() { 579 return getName(); 580 } 581 582 /** 583 * Returns the name of the source hierarchy, if this hierarchy is shared, 584 * otherwise null. 585 * 586 * <p>If this hierarchy is a public -- that is, it belongs to a dimension 587 * which is a usage of a shared dimension -- then 588 * <code>sharedHierarchyName</code> holds the unique name of the shared 589 * hierarchy; otherwise it is null. 590 * 591 * <p> Suppose this hierarchy is "Weekly" in the dimension "Order Date" of 592 * cube "Sales", and that "Order Date" is a usage of the "Time" 593 * dimension. Then <code>sharedHierarchyName</code> will be 594 * "[Time].[Weekly]". 595 */ 596 public String getSharedHierarchyName() { 597 return sharedHierarchyName; 598 } 599 600 /** 601 * Adds to the FROM clause of the query the tables necessary to access the 602 * members of this hierarchy in an inverse join order, used with agg tables. 603 * If <code>expression</code> is not null, adds the tables necessary to 604 * compute that expression. 605 * 606 * <p> This method is idempotent: if you call it more than once, it only 607 * adds the table(s) to the FROM clause once. 608 * 609 * @param query Query to add the hierarchy to 610 * @param expression Level to qualify up to; if null, qualifies up to the 611 * topmost ('all') expression, which may require more columns and more 612 * joins 613 */ 614 void addToFromInverse(SqlQuery query, MondrianDef.Expression expression) { 615 if (relation == null) { 616 throw Util.newError( 617 "cannot add hierarchy " + getUniqueName() 618 + " to query: it does not have a <Table>, <View> or <Join>"); 619 } 620 final boolean failIfExists = false; 621 MondrianDef.RelationOrJoin subRelation = relation; 622 if (relation instanceof MondrianDef.Join) { 623 if (expression != null) { 624 subRelation = 625 relationSubsetInverse(relation, expression.getTableAlias()); 626 } 627 } 628 query.addFrom(subRelation, null, failIfExists); 629 } 630 631 /** 632 * Adds to the FROM clause of the query the tables necessary to access the 633 * members of this hierarchy. If <code>expression</code> is not null, adds 634 * the tables necessary to compute that expression. 635 * 636 * <p> This method is idempotent: if you call it more than once, it only 637 * adds the table(s) to the FROM clause once. 638 * 639 * @param query Query to add the hierarchy to 640 * @param expression Level to qualify up to; if null, qualifies up to the 641 * topmost ('all') expression, which may require more columns and more 642 * joins 643 */ 644 void addToFrom(SqlQuery query, MondrianDef.Expression expression) { 645 if (relation == null) { 646 throw Util.newError( 647 "cannot add hierarchy " + getUniqueName() 648 + " to query: it does not have a <Table>, <View> or <Join>"); 649 } 650 query.registerRootRelation(relation); 651 final boolean failIfExists = false; 652 MondrianDef.RelationOrJoin subRelation = relation; 653 if (relation instanceof MondrianDef.Join) { 654 if (expression != null) { 655 // Suppose relation is 656 // (((A join B) join C) join D) 657 // and the fact table is 658 // F 659 // and our expression uses C. We want to make the expression 660 // F left join ((A join B) join C). 661 // Search for the smallest subset of the relation which 662 // uses C. 663 subRelation = 664 relationSubset(relation, expression.getTableAlias()); 665 } 666 } 667 query.addFrom( 668 subRelation, 669 expression == null ? null : expression.getTableAlias(), 670 failIfExists); 671 } 672 673 /** 674 * Adds a table to the FROM clause of the query. 675 * If <code>table</code> is not null, adds the table. Otherwise, add the 676 * relation on which this hierarchy is based on. 677 * 678 * <p> This method is idempotent: if you call it more than once, it only 679 * adds the table(s) to the FROM clause once. 680 * 681 * @param query Query to add the hierarchy to 682 * @param table table to add to the query 683 */ 684 void addToFrom(SqlQuery query, RolapStar.Table table) { 685 if (getRelation() == null) { 686 throw Util.newError( 687 "cannot add hierarchy " + getUniqueName() 688 + " to query: it does not have a <Table>, <View> or <Join>"); 689 } 690 final boolean failIfExists = false; 691 MondrianDef.RelationOrJoin subRelation = null; 692 if (table != null) { 693 // Suppose relation is 694 // (((A join B) join C) join D) 695 // and the fact table is 696 // F 697 // and the table to add is C. We want to make the expression 698 // F left join ((A join B) join C). 699 // Search for the smallest subset of the relation which 700 // joins with C. 701 subRelation = lookupRelationSubset(getRelation(), table); 702 } 703 704 if (subRelation == null) { 705 // If no table is found or specified, add the entire base relation. 706 subRelation = getRelation(); 707 } 708 709 boolean tableAdded = 710 query.addFrom( 711 subRelation, 712 table != null ? table.getAlias() : null, 713 failIfExists); 714 if (tableAdded && table != null) { 715 RolapStar.Condition joinCondition = table.getJoinCondition(); 716 if (joinCondition != null) { 717 query.addWhere(joinCondition); 718 } 719 } 720 } 721 722 /** 723 * Returns the smallest subset of <code>relation</code> which contains 724 * the relation <code>alias</code>, or null if these is no relation with 725 * such an alias, in inverse join order, used for agg tables. 726 * 727 * @param relation the relation in which to look for table by its alias 728 * @param alias table alias to search for 729 * @return the smallest containing relation or null if no matching table 730 * is found in <code>relation</code> 731 */ 732 private static MondrianDef.RelationOrJoin relationSubsetInverse( 733 MondrianDef.RelationOrJoin relation, 734 String alias) 735 { 736 if (relation instanceof MondrianDef.Relation) { 737 MondrianDef.Relation table = 738 (MondrianDef.Relation) relation; 739 return table.getAlias().equals(alias) 740 ? relation 741 : null; 742 743 } else if (relation instanceof MondrianDef.Join) { 744 MondrianDef.Join join = (MondrianDef.Join) relation; 745 MondrianDef.RelationOrJoin leftRelation = 746 relationSubsetInverse(join.left, alias); 747 return (leftRelation == null) 748 ? relationSubsetInverse(join.right, alias) 749 : join; 750 751 } else { 752 throw Util.newInternal("bad relation type " + relation); 753 } 754 } 755 756 /** 757 * Returns the smallest subset of <code>relation</code> which contains 758 * the relation <code>alias</code>, or null if these is no relation with 759 * such an alias. 760 * @param relation the relation in which to look for table by its alias 761 * @param alias table alias to search for 762 * @return the smallest containing relation or null if no matching table 763 * is found in <code>relation</code> 764 */ 765 private static MondrianDef.RelationOrJoin relationSubset( 766 MondrianDef.RelationOrJoin relation, 767 String alias) 768 { 769 if (relation instanceof MondrianDef.Relation) { 770 MondrianDef.Relation table = 771 (MondrianDef.Relation) relation; 772 return table.getAlias().equals(alias) 773 ? relation 774 : null; 775 776 } else if (relation instanceof MondrianDef.Join) { 777 MondrianDef.Join join = (MondrianDef.Join) relation; 778 MondrianDef.RelationOrJoin rightRelation = 779 relationSubset(join.right, alias); 780 return (rightRelation == null) 781 ? relationSubset(join.left, alias) 782 : MondrianProperties.instance() 783 .FilterChildlessSnowflakeMembers.get() 784 ? join 785 : rightRelation; 786 } else { 787 throw Util.newInternal("bad relation type " + relation); 788 } 789 } 790 791 /** 792 * Returns the smallest subset of <code>relation</code> which contains 793 * the table <code>targetTable</code>, or null if the targetTable is not 794 * one of the joining table in <code>relation</code>. 795 * 796 * @param relation the relation in which to look for targetTable 797 * @param targetTable table to add to the query 798 * @return the smallest containing relation or null if no matching table 799 * is found in <code>relation</code> 800 */ 801 private static MondrianDef.RelationOrJoin lookupRelationSubset( 802 MondrianDef.RelationOrJoin relation, 803 RolapStar.Table targetTable) 804 { 805 if (relation instanceof MondrianDef.Table) { 806 MondrianDef.Table table = (MondrianDef.Table) relation; 807 if (table.name.equals(targetTable.getTableName())) { 808 return relation; 809 } else { 810 // Not the same table if table names are different 811 return null; 812 } 813 } else if (relation instanceof MondrianDef.Join) { 814 // Search inside relation, starting from the rightmost table, 815 // and move left along the join chain. 816 MondrianDef.Join join = (MondrianDef.Join) relation; 817 MondrianDef.RelationOrJoin rightRelation = 818 lookupRelationSubset(join.right, targetTable); 819 if (rightRelation == null) { 820 // Keep searching left. 821 return lookupRelationSubset( 822 join.left, targetTable); 823 } else { 824 // Found a match. 825 return join; 826 } 827 } 828 return null; 829 } 830 831 /** 832 * Creates a member reader which enforces the access-control profile of 833 * <code>role</code>. 834 * 835 * <p>This method may not be efficient, so the caller should take care 836 * not to call it too often. A cache is a good idea. 837 * 838 * @param role Role 839 * @return Member reader that implements access control 840 * 841 * @pre role != null 842 * @post return != null 843 */ 844 MemberReader createMemberReader(Role role) { 845 final Access access = role.getAccess(this); 846 switch (access) { 847 case NONE: 848 role.getAccess(this); // todo: remove 849 throw Util.newInternal( 850 "Illegal access to members of hierarchy " + this); 851 case ALL: 852 return (isRagged()) 853 ? new SmartRestrictedMemberReader(getMemberReader(), role) 854 : getMemberReader(); 855 856 case CUSTOM: 857 final Role.HierarchyAccess hierarchyAccess = 858 role.getAccessDetails(this); 859 final Role.RollupPolicy rollupPolicy = 860 hierarchyAccess.getRollupPolicy(); 861 final NumericType returnType = new NumericType(); 862 switch (rollupPolicy) { 863 case FULL: 864 return new SmartRestrictedMemberReader( 865 getMemberReader(), role); 866 case PARTIAL: 867 Type memberType1 = 868 new mondrian.olap.type.MemberType( 869 getDimension(), 870 this, 871 null, 872 null); 873 SetType setType = new SetType(memberType1); 874 ListCalc listCalc = 875 new AbstractListCalc( 876 new DummyExp(setType), new Calc[0]) 877 { 878 public TupleList evaluateList( 879 Evaluator evaluator) 880 { 881 return 882 new UnaryTupleList( 883 getLowestMembersForAccess( 884 evaluator, hierarchyAccess, null)); 885 } 886 887 public boolean dependsOn(Hierarchy hierarchy) { 888 return true; 889 } 890 }; 891 final Calc partialCalc = 892 new LimitedRollupAggregateCalc(returnType, listCalc); 893 894 final Exp partialExp = 895 new ResolvedFunCall( 896 new FunDefBase("$x", "x", "In") { 897 public Calc compileCall( 898 ResolvedFunCall call, 899 ExpCompiler compiler) 900 { 901 return partialCalc; 902 } 903 904 public void unparse(Exp[] args, PrintWriter pw) { 905 pw.print("$RollupAccessibleChildren()"); 906 } 907 }, 908 new Exp[0], 909 returnType); 910 return new LimitedRollupSubstitutingMemberReader( 911 getMemberReader(), role, hierarchyAccess, partialExp); 912 913 case HIDDEN: 914 Exp hiddenExp = 915 new ResolvedFunCall( 916 new FunDefBase("$x", "x", "In") { 917 public Calc compileCall( 918 ResolvedFunCall call, ExpCompiler compiler) 919 { 920 return new ConstantCalc(returnType, null); 921 } 922 923 public void unparse(Exp[] args, PrintWriter pw) { 924 pw.print("$RollupAccessibleChildren()"); 925 } 926 }, 927 new Exp[0], 928 returnType); 929 return new LimitedRollupSubstitutingMemberReader( 930 getMemberReader(), role, hierarchyAccess, hiddenExp); 931 default: 932 throw Util.unexpected(rollupPolicy); 933 } 934 default: 935 throw Util.badValue(access); 936 } 937 } 938 939 /** 940 * Goes recursively down a hierarchy and builds a list of the 941 * members that should be constrained on because of access controls. 942 * It isn't sufficient to constrain on the current level in the 943 * evaluator because the actual constraint could be even more limited 944 * <p>Example. If we only give access to Seattle but the query is on 945 * the country level, we have to constrain at the city level, not state, 946 * or else all the values of all cities in the state will be returned. 947 */ 948 List<Member> getLowestMembersForAccess( 949 Evaluator evaluator, 950 HierarchyAccess hAccess, 951 Map<Member, Access> membersWithAccess) 952 { 953 if (membersWithAccess == null) { 954 membersWithAccess = 955 FunUtil.getNonEmptyMemberChildrenWithDetails( 956 evaluator, 957 ((RolapEvaluator) evaluator) 958 .getExpanding()); 959 } 960 boolean goesLower = false; 961 for (Member member : membersWithAccess.keySet()) { 962 Access access = membersWithAccess.get(member); 963 if (access == null) { 964 access = hAccess.getAccess(member); 965 } 966 if (access != Access.ALL) { 967 goesLower = true; 968 break; 969 } 970 } 971 if (goesLower) { 972 // We still have to go one more level down. 973 Map<Member, Access> newMap = 974 new HashMap<Member, Access>(); 975 for (Member member : membersWithAccess.keySet()) { 976 int savepoint = evaluator.savepoint(); 977 try { 978 evaluator.setContext(member); 979 newMap.putAll( 980 FunUtil.getNonEmptyMemberChildrenWithDetails( 981 evaluator, 982 member)); 983 } finally { 984 evaluator.restore(savepoint); 985 } 986 } 987 // Now pass it recursively to this method. 988 return getLowestMembersForAccess( 989 evaluator, hAccess, newMap); 990 } 991 return new ArrayList<Member>(membersWithAccess.keySet()); 992 } 993 994 /** 995 * A hierarchy is ragged if it contains one or more levels with hidden 996 * members. 997 */ 998 public boolean isRagged() { 999 for (Level level : levels) { 1000 if (((RolapLevel) level).getHideMemberCondition() 1001 != RolapLevel.HideMemberCondition.Never) 1002 { 1003 return true; 1004 } 1005 } 1006 return false; 1007 } 1008 1009 /** 1010 * Returns an expression which will compute a member's value by aggregating 1011 * its children. 1012 * 1013 * <p>It is efficient to share one expression between all calculated members 1014 * in a parent-child hierarchy, so we only need need to validate the 1015 * expression once. 1016 */ 1017 synchronized Exp getAggregateChildrenExpression() { 1018 if (aggregateChildrenExpression == null) { 1019 UnresolvedFunCall fc = new UnresolvedFunCall( 1020 "$AggregateChildren", 1021 Syntax.Internal, 1022 new Exp[] {new HierarchyExpr(this)}); 1023 Validator validator = 1024 Util.createSimpleValidator(BuiltinFunTable.instance()); 1025 aggregateChildrenExpression = fc.accept(validator); 1026 } 1027 return aggregateChildrenExpression; 1028 } 1029 1030 /** 1031 * Builds a dimension which maps onto a table holding the transitive 1032 * closure of the relationship for this parent-child level. 1033 * 1034 * <p>This method is triggered by the 1035 * {@link mondrian.olap.MondrianDef.Closure} element 1036 * in a schema, and is only meaningful for a parent-child hierarchy. 1037 * 1038 * <p>When a Schema contains a parent-child Hierarchy that has an 1039 * associated closure table, Mondrian creates a parallel internal 1040 * Hierarchy, called a "closed peer", that refers to the closure table. 1041 * This is indicated in the schema at the level of a Level, by including a 1042 * Closure element. The closure table represents 1043 * the transitive closure of the parent-child relationship. 1044 * 1045 * <p>The peer dimension, with its single hierarchy, and 3 levels (all, 1046 * closure, item) really 'belong to' the parent-child level. If a single 1047 * hierarchy had two parent-child levels (however unlikely this might be) 1048 * then each level would have its own auxiliary dimension. 1049 * 1050 * <p>For example, in the demo schema the [HR].[Employee] dimension 1051 * contains a parent-child hierarchy: 1052 * 1053 * <pre> 1054 * <Dimension name="Employees" foreignKey="employee_id"> 1055 * <Hierarchy hasAll="true" allMemberName="All Employees" 1056 * primaryKey="employee_id"> 1057 * <Table name="employee"/> 1058 * <Level name="Employee Id" type="Numeric" uniqueMembers="true" 1059 * column="employee_id" parentColumn="supervisor_id" 1060 * nameColumn="full_name" nullParentValue="0"> 1061 * <Closure parentColumn="supervisor_id" 1062 * childColumn="employee_id"> 1063 * <Table name="employee_closure"/> 1064 * </Closure> 1065 * ... 1066 * </pre> 1067 * The internal closed peer Hierarchy has this structure: 1068 * <pre> 1069 * <Dimension name="Employees" foreignKey="employee_id"> 1070 * ... 1071 * <Hierarchy name="Employees$Closure" 1072 * hasAll="true" allMemberName="All Employees" 1073 * primaryKey="employee_id" primaryKeyTable="employee_closure"> 1074 * <Join leftKey="supervisor_id" rightKey="employee_id"> 1075 * <Table name="employee_closure"/> 1076 * <Table name="employee"/> 1077 * </Join> 1078 * <Level name="Closure" type="Numeric" uniqueMembers="false" 1079 * table="employee_closure" column="supervisor_id"/> 1080 * <Level name="Employee" type="Numeric" uniqueMembers="true" 1081 * table="employee_closure" column="employee_id"/> 1082 * </Hierarchy> 1083 * </pre> 1084 * 1085 * <p>Note that the original Level with the Closure produces two Levels in 1086 * the closed peer Hierarchy: a simple peer (Employee) and a closed peer 1087 * (Closure). 1088 * 1089 * @param src a parent-child Level that has a Closure clause 1090 * @param clos a Closure clause 1091 * @return the closed peer Level in the closed peer Hierarchy 1092 */ 1093 RolapDimension createClosedPeerDimension( 1094 RolapLevel src, 1095 MondrianDef.Closure clos, 1096 MondrianDef.CubeDimension xmlDimension) 1097 { 1098 // REVIEW (mb): What about attribute primaryKeyTable? 1099 1100 // Create a peer dimension. 1101 RolapDimension peerDimension = new RolapDimension( 1102 dimension.getSchema(), 1103 dimension.getName() + "$Closure", 1104 null, 1105 true, 1106 "Closure dimension for parent-child hierarchy " + getName(), 1107 DimensionType.StandardDimension, 1108 dimension.isHighCardinality(), 1109 Collections.<String, Annotation>emptyMap()); 1110 1111 // Create a peer hierarchy. 1112 RolapHierarchy peerHier = peerDimension.newHierarchy(null, true, this); 1113 peerHier.allMemberName = getAllMemberName(); 1114 peerHier.allMember = (RolapMemberBase) getAllMember(); 1115 peerHier.allLevelName = getAllLevelName(); 1116 peerHier.sharedHierarchyName = getSharedHierarchyName(); 1117 MondrianDef.Join join = new MondrianDef.Join(); 1118 peerHier.relation = join; 1119 join.left = clos.table; // the closure table 1120 join.leftKey = clos.parentColumn; 1121 join.right = relation; // the unclosed base table 1122 join.rightKey = clos.childColumn; 1123 1124 // Create the upper level. 1125 // This represents all groups of descendants. For example, in the 1126 // Employee closure hierarchy, this level has a row for every employee. 1127 int index = peerHier.levels.length; 1128 int flags = src.getFlags() &~ RolapLevel.FLAG_UNIQUE; 1129 MondrianDef.Expression keyExp = 1130 new MondrianDef.Column(clos.table.name, clos.parentColumn); 1131 1132 RolapLevel level = 1133 new RolapLevel( 1134 peerHier, "Closure", caption, true, description, index++, 1135 keyExp, null, null, null, 1136 null, null, // no longer a parent-child hierarchy 1137 null, 1138 RolapProperty.emptyArray, 1139 flags | RolapLevel.FLAG_UNIQUE, 1140 src.getDatatype(), 1141 null, 1142 src.getHideMemberCondition(), 1143 src.getLevelType(), 1144 "", 1145 Collections.<String, Annotation>emptyMap()); 1146 peerHier.levels = Util.append(peerHier.levels, level); 1147 1148 // Create lower level. 1149 // This represents individual items. For example, in the Employee 1150 // closure hierarchy, this level has a row for every direct and 1151 // indirect report of every employee (which is more than the number 1152 // of employees). 1153 flags = src.getFlags() | RolapLevel.FLAG_UNIQUE; 1154 keyExp = new MondrianDef.Column(clos.table.name, clos.childColumn); 1155 RolapLevel sublevel = new RolapLevel( 1156 peerHier, 1157 "Item", 1158 null, 1159 true, 1160 null, 1161 index++, 1162 keyExp, 1163 null, 1164 null, 1165 null, 1166 null, 1167 null, // no longer a parent-child hierarchy 1168 null, 1169 RolapProperty.emptyArray, 1170 flags, 1171 src.getDatatype(), 1172 src.getInternalType(), 1173 src.getHideMemberCondition(), 1174 src.getLevelType(), 1175 "", 1176 Collections.<String, Annotation>emptyMap()); 1177 peerHier.levels = Util.append(peerHier.levels, sublevel); 1178 1179 return peerDimension; 1180 } 1181 1182 /** 1183 * Sets default member of this Hierarchy. 1184 * 1185 * @param defaultMember Default member 1186 */ 1187 public void setDefaultMember(Member defaultMember) { 1188 if (defaultMember != null) { 1189 this.defaultMember = defaultMember; 1190 } 1191 } 1192 1193 1194 /** 1195 * <p>Gets "unique key level name" attribute of this Hierarchy, if set. 1196 * If set, this property indicates that all level properties are 1197 * functionally dependent (invariant) on their associated levels, 1198 * and that the set of levels from the root to the named level (inclusive) 1199 * effectively defines an alternate key.</p> 1200 * 1201 * <p>This allows the GROUP BY to be eliminated from associated queries.</p> 1202 * 1203 * @return the name of the "unique key" level, or null if not specified 1204 */ 1205 public String getUniqueKeyLevelName() { 1206 return uniqueKeyLevelName; 1207 } 1208 1209 /** 1210 * Returns the ordinal of this hierarchy in its cube. 1211 * 1212 * <p>Temporarily defined against RolapHierarchy; will be moved to 1213 * RolapCubeHierarchy as soon as the measures hierarchy is a 1214 * RolapCubeHierarchy. 1215 * 1216 * @return Ordinal of this hierarchy in its cube 1217 */ 1218 public int getOrdinalInCube() { 1219 // This is temporary to verify that all calls to this method are for 1220 // the measures hierarchy. For all other hierarchies, the context will 1221 // be a RolapCubeHierarchy. 1222 // 1223 // In particular, if this method is called from 1224 // RolapEvaluator.setContext, the caller of that method should have 1225 // passed in a RolapCubeMember, not a RolapMember. 1226 assert dimension.isMeasures(); 1227 return 0; 1228 } 1229 1230 1231 /** 1232 * A <code>RolapNullMember</code> is the null member of its hierarchy. 1233 * Every hierarchy has precisely one. They are yielded by operations such as 1234 * <code>[Gender].[All].ParentMember</code>. Null members are usually 1235 * omitted from sets (in particular, in the set constructor operator "{ ... 1236 * }". 1237 */ 1238 static class RolapNullMember extends RolapMemberBase { 1239 RolapNullMember(final RolapLevel level) { 1240 super( 1241 null, 1242 level, 1243 RolapUtil.sqlNullValue, 1244 RolapUtil.mdxNullLiteral(), 1245 MemberType.NULL); 1246 assert level != null; 1247 } 1248 } 1249 1250 /** 1251 * Calculated member which is also a measure (that is, a member of the 1252 * [Measures] dimension). 1253 */ 1254 protected static class RolapCalculatedMeasure 1255 extends RolapCalculatedMember 1256 implements RolapMeasure 1257 { 1258 private RolapResult.ValueFormatter cellFormatter; 1259 1260 public RolapCalculatedMeasure( 1261 RolapMember parent, RolapLevel level, String name, Formula formula) 1262 { 1263 super(parent, level, name, formula); 1264 } 1265 1266 public synchronized void setProperty(String name, Object value) { 1267 if (name.equals(Property.CELL_FORMATTER.getName())) { 1268 String cellFormatterClass = (String) value; 1269 try { 1270 CellFormatter formatter = 1271 RolapSchema.getCellFormatter( 1272 cellFormatterClass, 1273 null); 1274 this.cellFormatter = 1275 new RolapResult.CellFormatterValueFormatter(formatter); 1276 } catch (Exception e) { 1277 throw MondrianResource.instance().CellFormatterLoadFailed 1278 .ex( 1279 cellFormatterClass, getUniqueName(), e); 1280 } 1281 } 1282 if (name.equals(Property.CELL_FORMATTER_SCRIPT.name)) { 1283 String language = (String) getPropertyValue( 1284 Property.CELL_FORMATTER_SCRIPT_LANGUAGE.name); 1285 String scriptText = (String) value; 1286 try { 1287 final Scripts.ScriptDefinition script = 1288 new Scripts.ScriptDefinition( 1289 scriptText, 1290 Scripts.ScriptLanguage.lookup(language)); 1291 CellFormatter formatter = 1292 RolapSchema.getCellFormatter( 1293 null, 1294 script); 1295 this.cellFormatter = 1296 new RolapResult.CellFormatterValueFormatter(formatter); 1297 } catch (Exception e) { 1298 throw MondrianResource.instance().CellFormatterLoadFailed 1299 .ex( 1300 scriptText, getUniqueName(), e); 1301 } 1302 } 1303 super.setProperty(name, value); 1304 } 1305 1306 public RolapResult.ValueFormatter getFormatter() { 1307 return cellFormatter; 1308 } 1309 } 1310 1311 /** 1312 * Substitute for a member in a hierarchy whose rollup policy is 'partial' 1313 * or 'hidden'. The member is calculated using an expression which 1314 * aggregates only visible descendants. 1315 * 1316 * <p>Note that this class extends RolapCubeMember only because other code 1317 * expects that all members in a RolapCubeHierarchy are RolapCubeMembers. 1318 * As part of {@link mondrian.util.Bug#BugSegregateRolapCubeMemberFixed}, 1319 * maybe make {@link mondrian.rolap.RolapCubeMember} an interface. 1320 * 1321 * @see mondrian.olap.Role.RollupPolicy 1322 */ 1323 public static class LimitedRollupMember extends RolapCubeMember { 1324 public final RolapMember member; 1325 private final Exp exp; 1326 final HierarchyAccess hierarchyAccess; 1327 1328 LimitedRollupMember( 1329 RolapCubeMember member, 1330 Exp exp, 1331 HierarchyAccess hierarchyAccess) 1332 { 1333 super( 1334 member.getParentMember(), 1335 member.getRolapMember(), 1336 member.getLevel()); 1337 this.hierarchyAccess = hierarchyAccess; 1338 assert !(member instanceof LimitedRollupMember); 1339 this.member = member; 1340 this.exp = exp; 1341 } 1342 1343 public boolean equals(Object o) { 1344 return o instanceof LimitedRollupMember 1345 && ((LimitedRollupMember) o).member.equals(member); 1346 } 1347 1348 public int hashCode() { 1349 int hash = member.hashCode(); 1350 hash = Util.hash(hash, exp); 1351 return hash; 1352 } 1353 1354 public Exp getExpression() { 1355 return exp; 1356 } 1357 1358 protected boolean computeCalculated(final MemberType memberType) { 1359 return true; 1360 } 1361 1362 public boolean isCalculated() { 1363 return false; 1364 } 1365 1366 public boolean isEvaluated() { 1367 return true; 1368 } 1369 } 1370 1371 /** 1372 * Member reader which wraps a hierarchy's member reader, and if the 1373 * role has limited access to the hierarchy, replaces members with 1374 * dummy members which evaluate to the sum of only the accessible children. 1375 */ 1376 private static class LimitedRollupSubstitutingMemberReader 1377 extends SubstitutingMemberReader 1378 { 1379 private final Role.HierarchyAccess hierarchyAccess; 1380 private final Exp exp; 1381 1382 /** 1383 * Creates a LimitedRollupSubstitutingMemberReader. 1384 * 1385 * @param memberReader Underlying member reader 1386 * @param role Role to enforce 1387 * @param hierarchyAccess Access this role has to the hierarchy 1388 * @param exp Expression for hidden member 1389 */ 1390 public LimitedRollupSubstitutingMemberReader( 1391 MemberReader memberReader, 1392 Role role, 1393 Role.HierarchyAccess hierarchyAccess, 1394 Exp exp) 1395 { 1396 super( 1397 new SmartRestrictedMemberReader( 1398 memberReader, role)); 1399 this.hierarchyAccess = hierarchyAccess; 1400 this.exp = exp; 1401 } 1402 1403 public Map<? extends Member, Access> getMemberChildren( 1404 RolapMember member, 1405 List<RolapMember> memberChildren, 1406 MemberChildrenConstraint constraint) 1407 { 1408 return memberReader.getMemberChildren( 1409 member, 1410 new SubstitutingMemberList(memberChildren), 1411 constraint); 1412 } 1413 1414 public Map<? extends Member, Access> getMemberChildren( 1415 List<RolapMember> parentMembers, 1416 List<RolapMember> children, 1417 MemberChildrenConstraint constraint) 1418 { 1419 return memberReader.getMemberChildren( 1420 parentMembers, 1421 new SubstitutingMemberList(children), 1422 constraint); 1423 } 1424 1425 public RolapMember substitute(RolapMember member, Access access) { 1426 if (member != null 1427 && member instanceof MultiCardinalityDefaultMember) 1428 { 1429 return new LimitedRollupMember( 1430 (RolapCubeMember) 1431 ((MultiCardinalityDefaultMember) member) 1432 .member.getParentMember(), 1433 exp, 1434 hierarchyAccess); 1435 } 1436 if (member != null 1437 && (access == Access.CUSTOM || hierarchyAccess 1438 .hasInaccessibleDescendants(member))) 1439 { 1440 // Member is visible, but at least one of its 1441 // descendants is not. 1442 if (member instanceof LimitedRollupMember) { 1443 member = ((LimitedRollupMember) member).member; 1444 } 1445 return new LimitedRollupMember( 1446 (RolapCubeMember) member, 1447 exp, 1448 hierarchyAccess); 1449 } else { 1450 // No need to substitute. Member and all of its 1451 // descendants are accessible. Total for member 1452 // is same as for FULL policy. 1453 return member; 1454 } 1455 } 1456 1457 public RolapMember substitute(final RolapMember member) { 1458 if (member == null) { 1459 return null; 1460 } 1461 return substitute(member, hierarchyAccess.getAccess(member)); 1462 } 1463 1464 @Override 1465 public RolapMember desubstitute(RolapMember member) { 1466 if (member instanceof LimitedRollupMember) { 1467 return ((LimitedRollupMember) member).member; 1468 } else { 1469 return member; 1470 } 1471 } 1472 } 1473 1474 /** 1475 * Compiled expression that computes rollup over a set of visible children. 1476 * The {@code listCalc} expression determines that list of children. 1477 */ 1478 private static class LimitedRollupAggregateCalc 1479 extends AggregateFunDef.AggregateCalc 1480 { 1481 public LimitedRollupAggregateCalc( 1482 Type returnType, 1483 ListCalc listCalc) 1484 { 1485 super( 1486 new DummyExp(returnType), 1487 listCalc, 1488 new ValueCalc(new DummyExp(returnType))); 1489 } 1490 } 1491 1492 /** 1493 * Dummy element that acts as a namespace for resolving member names within 1494 * shared hierarchies. Acts like a cube that has a single child, the 1495 * hierarchy in question. 1496 */ 1497 private class DummyElement implements OlapElement { 1498 public String getUniqueName() { 1499 throw new UnsupportedOperationException(); 1500 } 1501 1502 public String getName() { 1503 return "$"; 1504 } 1505 1506 public String getDescription() { 1507 throw new UnsupportedOperationException(); 1508 } 1509 1510 public OlapElement lookupChild( 1511 SchemaReader schemaReader, 1512 Id.Segment s, 1513 MatchType matchType) 1514 { 1515 if (!(s instanceof Id.NameSegment)) { 1516 return null; 1517 } 1518 final Id.NameSegment nameSegment = (Id.NameSegment) s; 1519 1520 if (Util.equalName(nameSegment.name, dimension.getName())) { 1521 return dimension; 1522 } 1523 // Archaic form <dimension>.<hierarchy>, e.g. [Time.Weekly].[1997] 1524 if (!MondrianProperties.instance().SsasCompatibleNaming.get() 1525 && Util.equalName( 1526 nameSegment.name, 1527 dimension.getName() + "." + subName)) 1528 { 1529 return RolapHierarchy.this; 1530 } 1531 return null; 1532 } 1533 1534 public String getQualifiedName() { 1535 throw new UnsupportedOperationException(); 1536 } 1537 1538 public String getCaption() { 1539 throw new UnsupportedOperationException(); 1540 } 1541 1542 public Hierarchy getHierarchy() { 1543 throw new UnsupportedOperationException(); 1544 } 1545 1546 public Dimension getDimension() { 1547 throw new UnsupportedOperationException(); 1548 } 1549 1550 public boolean isVisible() { 1551 throw new UnsupportedOperationException(); 1552 } 1553 1554 public String getLocalized(LocalizedProperty prop, Locale locale) { 1555 throw new UnsupportedOperationException(); 1556 } 1557 } 1558} 1559 1560// End RolapHierarchy.java