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     * &lt;Dimension name="Employees" foreignKey="employee_id"&gt;
1055     *   &lt;Hierarchy hasAll="true" allMemberName="All Employees"
1056     *         primaryKey="employee_id"&gt;
1057     *     &lt;Table name="employee"/&gt;
1058     *     &lt;Level name="Employee Id" type="Numeric" uniqueMembers="true"
1059     *            column="employee_id" parentColumn="supervisor_id"
1060     *            nameColumn="full_name" nullParentValue="0"&gt;
1061     *       &lt;Closure parentColumn="supervisor_id"
1062     *                   childColumn="employee_id"&gt;
1063     *          &lt;Table name="employee_closure"/&gt;
1064     *       &lt;/Closure&gt;
1065     *       ...
1066     * </pre>
1067     * The internal closed peer Hierarchy has this structure:
1068     * <pre>
1069     * &lt;Dimension name="Employees" foreignKey="employee_id"&gt;
1070     *     ...
1071     *     &lt;Hierarchy name="Employees$Closure"
1072     *         hasAll="true" allMemberName="All Employees"
1073     *         primaryKey="employee_id" primaryKeyTable="employee_closure"&gt;
1074     *       &lt;Join leftKey="supervisor_id" rightKey="employee_id"&gt;
1075     *         &lt;Table name="employee_closure"/&gt;
1076     *         &lt;Table name="employee"/&gt;
1077     *       &lt;/Join&gt;
1078     *       &lt;Level name="Closure"  type="Numeric" uniqueMembers="false"
1079     *           table="employee_closure" column="supervisor_id"/&gt;
1080     *       &lt;Level name="Employee" type="Numeric" uniqueMembers="true"
1081     *           table="employee_closure" column="employee_id"/&gt;
1082     *     &lt;/Hierarchy&gt;
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