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