001    /*
002    // $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/CacheControlImpl.java#3 $
003    // This software is subject to the terms of the Common Public License
004    // Agreement, available at the following URL:
005    // http://www.opensource.org/licenses/cpl.html.
006    // Copyright (C) 2006-2008 Julian Hyde and others.
007    // All Rights Reserved.
008    // You must accept the terms of that agreement to use this software.
009    */
010    package mondrian.rolap;
011    
012    import mondrian.olap.*;
013    import mondrian.resource.MondrianResource;
014    import mondrian.olap.CacheControl;
015    
016    import javax.sql.DataSource;
017    import java.util.*;
018    import java.io.PrintWriter;
019    
020    import org.eigenbase.util.property.BooleanProperty;
021    
022    /**
023     * Implementation of {@link CacheControl} API.
024     *
025     * @author jhyde
026     * @version $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/CacheControlImpl.java#3 $
027     * @since Sep 27, 2006
028     */
029    public class CacheControlImpl implements CacheControl {
030    
031        /**
032         * Object to lock before making changes to the member cache.
033         *
034         * <p>The "member cache" is a figure of speech: each RolapHierarchy has its
035         * own MemberCache object. But to provide transparently serialized access
036         * to the "member cache" via the interface CacheControl, provide a common
037         * lock here.
038         *
039         * <p>NOTE: static member is a little too wide a scope for this lock,
040         * because in theory a JVM can contain multiple independent instances of
041         * mondrian.
042         */
043        private static final Object MEMBER_CACHE_LOCK = new Object();
044        
045        // cell cache control
046        public CellRegion createMemberRegion(Member member, boolean descendants) {
047            if (member == null) {
048                throw new NullPointerException();
049            }
050            final ArrayList<Member> list = new ArrayList<Member>();
051            list.add(member);
052            return new MemberCellRegion(list, descendants);
053        }
054    
055        public CellRegion createMemberRegion(
056            boolean lowerInclusive,
057            Member lowerMember,
058            boolean upperInclusive,
059            Member upperMember,
060            boolean descendants)
061        {
062            if (lowerMember == null) {
063                lowerInclusive = false;
064            }
065            if (upperMember == null) {
066                upperInclusive = false;
067            }
068            return new MemberRangeCellRegion(
069                (RolapMember) lowerMember, lowerInclusive,
070                (RolapMember) upperMember, upperInclusive,
071                descendants);
072        }
073    
074        public CellRegion createCrossjoinRegion(CellRegion... regions) {
075            assert regions != null;
076            assert regions.length >= 2;
077            final HashSet<Dimension> set = new HashSet<Dimension>();
078            final List<CellRegionImpl> list = new ArrayList<CellRegionImpl>();
079            for (CellRegion region : regions) {
080                int prevSize = set.size();
081                List<Dimension> dimensionality = region.getDimensionality();
082                set.addAll(dimensionality);
083                if (set.size() < prevSize + dimensionality.size()) {
084                    throw MondrianResource.instance().
085                        CacheFlushCrossjoinDimensionsInCommon.ex(
086                        getDimensionalityList(regions));
087                }
088    
089                flattenCrossjoin((CellRegionImpl) region, list);
090            }
091            return new CrossjoinCellRegion(list);
092        }
093    
094        // Returns e.g. "'[[Product]]', '[[Time], [Product]]'"
095        private String getDimensionalityList(CellRegion[] regions) {
096            StringBuilder buf = new StringBuilder();
097            int k = 0;
098            for (CellRegion region : regions) {
099                if (k++ > 0) {
100                    buf.append(", ");
101                }
102                buf.append("'");
103                buf.append(region.getDimensionality().toString());
104                buf.append("'");
105            }
106            return buf.toString();
107        }
108    
109        public CellRegion createUnionRegion(CellRegion... regions)
110        {
111            if (regions == null) {
112                throw new NullPointerException();
113            }
114            if (regions.length < 2) {
115                throw new IllegalArgumentException();
116            }
117            final List<CellRegionImpl> list = new ArrayList<CellRegionImpl>();
118            for (CellRegion region : regions) {
119                if (!region.getDimensionality().equals(
120                    regions[0].getDimensionality())) {
121                    throw MondrianResource.instance().
122                        CacheFlushUnionDimensionalityMismatch.ex(
123                        regions[0].getDimensionality().toString(),
124                        region.getDimensionality().toString());
125                }
126                list.add((CellRegionImpl) region);
127            }
128            return new UnionCellRegion(list);
129        }
130    
131        public CellRegion createMeasuresRegion(Cube cube) {
132            final Dimension measuresDimension = cube.getDimensions()[0];
133            final Member[] measures =
134                cube.getSchemaReader(null).getLevelMembers(
135                    measuresDimension.getHierarchy().getLevels()[0],
136                    false);
137            return new MemberCellRegion(Arrays.asList(measures), false);
138        }
139    
140        public void flush(CellRegion region) {
141            final List<Dimension> dimensionality = region.getDimensionality();
142            boolean found = false;
143            for (Dimension dimension : dimensionality) {
144                if (dimension.isMeasures()) {
145                    found = true;
146                    break;
147                }
148            }
149            if (!found) {
150                throw MondrianResource.instance().
151                    CacheFlushRegionMustContainMembers.ex();
152            }
153            final UnionCellRegion union = normalize((CellRegionImpl) region);
154            for (CellRegionImpl cellRegion : union.regions) {
155                // Figure out the bits.
156                flushNonUnion(cellRegion);
157            }
158        }
159    
160        /**
161         * Flushes a list of cell regions.
162         *
163         * @param cellRegionList List of cell regions
164         */
165        protected void flushRegionList(List<CellRegion> cellRegionList) {
166            final CellRegion cellRegion;
167            switch (cellRegionList.size()) {
168            case 0:
169                return;
170            case 1:
171                cellRegion = cellRegionList.get(0);
172                break;
173            default:
174                final CellRegion[] cellRegions =
175                    cellRegionList.toArray(new CellRegion[cellRegionList.size()]);
176                cellRegion = createUnionRegion(cellRegions);
177                break;
178            }
179            flush(cellRegion);
180        }
181    
182        public void trace(String message) {
183            // ignore message
184        }
185    
186        public void flushSchemaCache() {
187            RolapSchema.Pool.instance().clear();
188        }
189    
190        // todo: document
191        public void flushSchema(
192            String catalogUrl,
193            String connectionKey,
194            String jdbcUser,
195            String dataSourceStr)
196        {
197            RolapSchema.Pool.instance().remove(
198                catalogUrl,
199                connectionKey,
200                jdbcUser,
201                dataSourceStr);
202        }
203    
204        // todo: document
205        public void flushSchema(
206            String catalogUrl,
207            DataSource dataSource)
208        {
209            RolapSchema.Pool.instance().remove(
210                catalogUrl,
211                dataSource);
212        }
213    
214        /**
215         * Flushes the given RolapSchema instance from the pool
216         *
217         * @param schema RolapSchema
218         */
219        public void flushSchema(Schema schema) {
220            if (RolapSchema.class.isInstance(schema)) {
221                RolapSchema.Pool.instance().remove((RolapSchema)schema);
222            } else {
223                throw new UnsupportedOperationException(schema.getClass().getName()+
224                        " cannot be flushed");
225            }
226        }
227    
228        protected void flushNonUnion(CellRegion region) {
229            throw new UnsupportedOperationException();
230        }
231    
232        /**
233         * Normalizes a CellRegion into a union of crossjoins of member regions.
234         *
235         * @param region Region
236         * @return normalized region
237         */
238        UnionCellRegion normalize(CellRegionImpl region) {
239            // Search for Union within a Crossjoin.
240            //   Crossjoin(a1, a2, Union(r1, r2, r3), a4)
241            // becomes
242            //   Union(
243            //     Crossjoin(a1, a2, r1, a4),
244            //     Crossjoin(a1, a2, r2, a4),
245            //     Crossjoin(a1, a2, r3, a4))
246    
247            // First, decompose into a flat list of non-union regions.
248            List<CellRegionImpl> nonUnionList = new LinkedList<CellRegionImpl>();
249            flattenUnion(region, nonUnionList);
250    
251            for (int i = 0; i < nonUnionList.size(); i++) {
252                while (true) {
253                    CellRegionImpl nonUnionRegion = nonUnionList.get(i);
254                    UnionCellRegion firstUnion = findFirstUnion(nonUnionRegion);
255                    if (firstUnion == null) {
256                        break;
257                    }
258                    List<CellRegionImpl> list = new ArrayList<CellRegionImpl>();
259                    for (CellRegionImpl unionComponent : firstUnion.regions) {
260                        // For each unionComponent in (r1, r2, r3),
261                        // create Crossjoin(a1, a2, r1, a4).
262                        CellRegionImpl cj =
263                            copyReplacing(
264                                nonUnionRegion,
265                                firstUnion,
266                                unionComponent);
267                        list.add(cj);
268                    }
269                    // Replace one element which contained a union with several
270                    // which contain one fewer union. (Double-linked list helps
271                    // here.)
272                    nonUnionList.remove(i);
273                    nonUnionList.addAll(i, list);
274                }
275            }
276            return new UnionCellRegion(nonUnionList);
277        }
278    
279        private CellRegionImpl copyReplacing(
280            CellRegionImpl region,
281            CellRegionImpl seek,
282            CellRegionImpl replacement)
283        {
284            if (region == seek) {
285                return replacement;
286            }
287            if (region instanceof UnionCellRegion) {
288                final UnionCellRegion union = (UnionCellRegion) region;
289                List<CellRegionImpl> list = new ArrayList<CellRegionImpl>();
290                for (CellRegionImpl child : union.regions) {
291                    list.add(copyReplacing(child, seek, replacement));
292                }
293                return new UnionCellRegion(list);
294            }
295            if (region instanceof CrossjoinCellRegion) {
296                final CrossjoinCellRegion crossjoin = (CrossjoinCellRegion) region;
297                List<CellRegionImpl> list = new ArrayList<CellRegionImpl>();
298                for (CellRegionImpl child : crossjoin.components) {
299                    list.add(copyReplacing(child, seek, replacement));
300                }
301                return new CrossjoinCellRegion(list);
302            }
303            // This region is atomic, and since regions are immutable we don't need
304            // to clone.
305            return region;
306        }
307    
308        /**
309         * Flatten a region into a list of regions none of which are unions.
310         *
311         * @param region Cell region
312         * @param list Target list
313         */
314        private void flattenUnion(
315            CellRegionImpl region,
316            List<CellRegionImpl> list)
317        {
318            if (region instanceof UnionCellRegion) {
319                UnionCellRegion union = (UnionCellRegion) region;
320                for (CellRegionImpl region1 : union.regions) {
321                    flattenUnion(region1, list);
322                }
323            } else {
324                list.add(region);
325            }
326        }
327    
328        /**
329         * Flattens a region into a list of regions none of which are unions.
330         *
331         * @param region Cell region
332         * @param list Target list
333         */
334        private void flattenCrossjoin(
335            CellRegionImpl region,
336            List<CellRegionImpl> list)
337        {
338            if (region instanceof CrossjoinCellRegion) {
339                CrossjoinCellRegion crossjoin = (CrossjoinCellRegion) region;
340                for (CellRegionImpl component : crossjoin.components) {
341                    flattenCrossjoin(component, list);
342                }
343            } else {
344                list.add(region);
345            }
346        }
347    
348        private UnionCellRegion findFirstUnion(CellRegion region) {
349            final CellRegionVisitor visitor =
350                new CellRegionVisitorImpl() {
351                    public void visit(UnionCellRegion region) {
352                        throw new FoundOne(region);
353                    }
354                };
355            try {
356                ((CellRegionImpl) region).accept(visitor);
357                return null;
358            } catch (FoundOne foundOne) {
359                return foundOne.region;
360            }
361        }
362    
363        /**
364         * Returns a list of members of the Measures dimension which are mentioned
365         * somewhere in a region specification.
366         *
367         * @param region Cell region
368         * @return List of members mentioned in cell region specification
369         */
370        static List<Member> findMeasures(CellRegion region) {
371            final List<Member> list = new ArrayList<Member>();
372            final CellRegionVisitor visitor =
373                new CellRegionVisitorImpl() {
374                    public void visit(MemberCellRegion region) {
375                        if (region.dimension.isMeasures()) {
376                            list.addAll(region.memberList);
377                        }
378                    }
379    
380                    public void visit(MemberRangeCellRegion region) {
381                        if (region.level.getDimension().isMeasures()) {
382                            // FIXME: don't allow range on measures dimension
383                            assert false : "ranges on measures dimension";
384                        }
385                    }
386                };
387            ((CellRegionImpl) region).accept(visitor);
388            return list;
389        }
390    
391        static List<RolapStar> getStarList(CellRegion region) {
392            // Figure out which measure (therefore star) it belongs to.
393            List<RolapStar> starList = new ArrayList<RolapStar>();
394            final List<Member> measuresList = findMeasures(region);
395            for (Member member : measuresList) {
396                RolapStoredMeasure measure = (RolapStoredMeasure) member;
397                final RolapStar.Measure starMeasure =
398                    (RolapStar.Measure) measure.getStarMeasure();
399                if (!starList.contains(starMeasure.getStar())) {
400                    starList.add(starMeasure.getStar());
401                }
402            }
403            return starList;
404        }
405    
406        public void printCacheState(
407            PrintWriter pw,
408            CellRegion region)
409        {
410            List<RolapStar> starList = getStarList(region);
411            for (RolapStar star : starList) {
412                star.print(pw, "", false);
413            }
414        }
415    
416        public MemberSet createMemberSet(Member member, boolean descendants)
417        {
418            return new SimpleMemberSet(
419                Collections.singletonList((RolapMember) member),
420                descendants);
421        }
422    
423        public MemberSet createMemberSet(
424            boolean lowerInclusive,
425            Member lowerMember,
426            boolean upperInclusive,
427            Member upperMember,
428            boolean descendants)
429        {
430            // TODO check that upperMember & lowerMember are in same Level
431            if (lowerMember == null) {
432                lowerInclusive = false;
433            }
434            if (upperMember == null) {
435                upperInclusive = false;
436            }
437            return new RangeMemberSet(
438                stripMember((RolapMember) lowerMember), lowerInclusive,
439                stripMember((RolapMember) upperMember), upperInclusive,
440                descendants);
441        }
442    
443        public MemberSet createUnionSet(MemberSet... args)
444        {
445            //noinspection unchecked
446            return new UnionMemberSet((List) Arrays.asList(args));
447        }
448    
449        public MemberSet filter(Level level, MemberSet baseSet) {
450            if (level instanceof RolapCubeLevel) {
451                // be forgiving
452                level = ((RolapCubeLevel) level).getRolapLevel();
453            }
454            return ((MemberSetPlus) baseSet).filter((RolapLevel) level);
455        }
456    
457        public void flush(MemberSet memberSet) {
458            // REVIEW How is flush(s) different to executing createDeleteCommand(s) ?
459            synchronized (MEMBER_CACHE_LOCK) {
460                final List<CellRegion> cellRegionList = new ArrayList<CellRegion>();
461                ((MemberSetPlus) memberSet).accept(
462                    new MemberSetVisitorImpl() {
463                        public void visit(RolapMember member) {
464                            flushMember(member, cellRegionList);
465                        }
466                    }
467                );
468                // STUB: flush the set: another visitor
469    
470                // finally, flush cells now invalid
471                flushRegionList(cellRegionList);
472            }
473        }
474    
475        public void printCacheState(PrintWriter pw, MemberSet set)
476        {
477            synchronized (MEMBER_CACHE_LOCK) {
478                pw.println("need to implement printCacheState"); // TODO:
479            }
480        }
481    
482        public MemberEditCommand createCompoundCommand(
483            List<MemberEditCommand> commandList)
484        {
485            //noinspection unchecked
486            return new CompoundCommand((List) commandList);
487        }
488    
489        public MemberEditCommand createCompoundCommand(
490            MemberEditCommand... commands)
491        {
492            //noinspection unchecked
493            return new CompoundCommand((List) Arrays.asList(commands));
494        }
495    
496        public MemberEditCommand createDeleteCommand(Member member) {
497            if (member == null) {
498                throw new IllegalArgumentException("cannot delete null member");
499            }
500            if (((RolapLevel) member.getLevel()).isParentChild()) {
501                throw new IllegalArgumentException(
502                    "delete member not supported for parent-child hierarchy");
503            }
504            return createDeleteCommand(createMemberSet(member, false));
505        }
506    
507        public MemberEditCommand createDeleteCommand(MemberSet s) {
508            return new DeleteMemberCommand((MemberSetPlus) s);
509        }
510    
511        public MemberEditCommand createAddCommand(
512            Member member) throws IllegalArgumentException
513        {
514            if (member == null) {
515                throw new IllegalArgumentException("cannot add null member");
516            }
517            if (((RolapLevel) member.getLevel()).isParentChild()) {
518                throw new IllegalArgumentException(
519                    "add member not supported for parent-child hierarchy");
520            }
521            return new AddMemberCommand((RolapMember) member);
522        }
523    
524        public MemberEditCommand createMoveCommand(Member member, Member loc)
525            throws IllegalArgumentException
526        {
527            if (member == null) {
528                throw new IllegalArgumentException("cannot move null member");
529            }
530            if (((RolapLevel) member.getLevel()).isParentChild()) {
531                throw new IllegalArgumentException(
532                    "move member not supported for parent-child hierarchy");
533            }
534            if (loc == null) {
535                throw new IllegalArgumentException("cannot move member to null location");
536            }
537            // TODO: check that MEMBER and LOC (its new parent) have appropriate Levels
538            return new MoveMemberCommand((RolapMember) member, (RolapMember) loc);
539        }
540    
541        public MemberEditCommand createSetPropertyCommand(
542            Member member,
543            String name,
544            Object value)
545            throws IllegalArgumentException
546        {
547            if (member == null) {
548                throw new IllegalArgumentException("cannot set properties on null member");
549            }
550            if (((RolapLevel) member.getLevel()).isParentChild()) {
551                throw new IllegalArgumentException(
552                    "set properties not supported for parent-child hierarchy");
553            }
554            // TODO: validate that prop NAME exists for Level of MEMBER
555            return new ChangeMemberPropsCommand(
556                new SimpleMemberSet(
557                    Collections.singletonList((RolapMember) member),
558                    false),
559                Collections.singletonMap(name, value));
560        }
561    
562        public MemberEditCommand createSetPropertyCommand(
563            MemberSet members,
564            Map<String, Object> propertyValues)
565            throws IllegalArgumentException
566        {
567            // TODO: check that members all at same Level, and validate that props exist
568            validateSameLevel((MemberSetPlus) members);
569            return new ChangeMemberPropsCommand(
570                (MemberSetPlus) members,
571                propertyValues);
572        }
573    
574        /**
575         * Validates that all members of a member set are the same level.
576         *
577         * @param memberSet Member set
578         * @throws IllegalArgumentException if members are from more than one level
579         */
580        private void validateSameLevel(MemberSetPlus memberSet)
581            throws IllegalArgumentException
582        {
583            memberSet.accept(
584                new MemberSetVisitor() {
585                    final Set<RolapLevel> levelSet = new HashSet<RolapLevel>();
586    
587                    private void visitMember(
588                        RolapMember member,
589                        boolean descendants)
590                    {
591                        final String message =
592                            "all members in set must belong to same level";
593                        if (levelSet.add(member.getLevel())
594                            && levelSet.size() > 1) {
595                            throw new IllegalArgumentException(message);
596                        }
597                        if (descendants
598                            && member.getLevel().getChildLevel() != null) {
599                            throw new IllegalArgumentException(message);
600                        }
601                    }
602    
603                    public void visit(SimpleMemberSet simpleMemberSet) {
604                        for (RolapMember member : simpleMemberSet.members) {
605                            visitMember(member, simpleMemberSet.descendants);
606                        }
607                    }
608    
609                    public void visit(UnionMemberSet unionMemberSet) {
610                        for (MemberSetPlus item : unionMemberSet.items) {
611                            item.accept(this);
612                        }
613                    }
614    
615                    public void visit(RangeMemberSet rangeMemberSet) {
616                        visitMember(
617                            rangeMemberSet.lowerMember,
618                            rangeMemberSet.descendants);
619                        visitMember(
620                            rangeMemberSet.upperMember,
621                            rangeMemberSet.descendants);
622                    }
623                }
624            );
625        }
626    
627        public void execute(MemberEditCommand cmd) {
628            final BooleanProperty prop =
629                MondrianProperties.instance().EnableRolapCubeMemberCache;
630            if (prop.get()) {
631                throw new IllegalArgumentException(
632                    "Member cache control operations are not allowed unless "
633                        + "property " + prop.getPath() + " is false");
634            }
635            synchronized (MEMBER_CACHE_LOCK) {
636                final List<CellRegion> cellRegionList =
637                    new ArrayList<CellRegion>();
638                ((MemberEditCommandPlus) cmd).execute(cellRegionList);
639                if (false) {
640                    // TODO: Flushing regions currently fails with the error
641                    // "Region of cells to be flushed must contain measures". This
642                    // method should receive a cube (or cubes) whose measures to
643                    // flush.
644                    flushRegionList(cellRegionList);
645                }
646            }
647        }
648    
649        private static MemberCache getMemberCache(RolapMember member) {
650            final MemberReader memberReader =
651                member.getHierarchy().getMemberReader();
652            SmartMemberReader smartMemberReader =
653                (SmartMemberReader) memberReader;
654            return smartMemberReader.getMemberCache();
655        }
656    
657        // cell cache control implementation
658    
659        /**
660         * Cell region formed by a list of members.
661         *
662         * @see MemberRangeCellRegion
663         */
664        static class MemberCellRegion implements CellRegionImpl {
665            private final List<Member> memberList;
666            private final Dimension dimension;
667            private final boolean descendants;
668    
669            MemberCellRegion(List<Member> memberList, boolean descendants) {
670                assert memberList.size() > 0;
671                this.memberList = memberList;
672                this.dimension = (memberList.get(0)).getDimension();
673                this.descendants = descendants;
674            }
675    
676            public List<Dimension> getDimensionality() {
677                return Collections.singletonList(dimension);
678            }
679    
680            public String toString() {
681                return Util.commaList("Member", memberList);
682            }
683    
684            public void accept(CellRegionVisitor visitor) {
685                visitor.visit(this);
686            }
687    
688            public List<Member> getMemberList() {
689                return memberList;
690            }
691        }
692    
693        /**
694         * Cell region formed a range of members between a lower and upper bound.
695         */
696        static class MemberRangeCellRegion implements CellRegionImpl {
697            private final RolapMember lowerMember;
698            private final boolean lowerInclusive;
699            private final RolapMember upperMember;
700            private final boolean upperInclusive;
701            private final boolean descendants;
702            private final RolapLevel level;
703    
704            MemberRangeCellRegion(
705                RolapMember lowerMember,
706                boolean lowerInclusive,
707                RolapMember upperMember,
708                boolean upperInclusive,
709                boolean descendants)
710            {
711                assert lowerMember != null || upperMember != null;
712                assert lowerMember == null
713                    || upperMember == null
714                    || lowerMember.getLevel() == upperMember.getLevel();
715                assert !(lowerMember == null && lowerInclusive);
716                assert !(upperMember == null && upperInclusive);
717                this.lowerMember = lowerMember;
718                this.lowerInclusive = lowerInclusive;
719                this.upperMember = upperMember;
720                this.upperInclusive = upperInclusive;
721                this.descendants = descendants;
722                this.level = lowerMember == null ?
723                    upperMember.getLevel() :
724                    lowerMember.getLevel();
725            }
726    
727            public List<Dimension> getDimensionality() {
728                return Collections.singletonList(level.getDimension());
729            }
730    
731            public RolapLevel getLevel() {
732                return level;
733            }
734    
735            public String toString() {
736                final StringBuilder sb = new StringBuilder("Range(");
737                if (lowerMember == null) {
738                    sb.append("null");
739                } else {
740                    sb.append(lowerMember);
741                    if (lowerInclusive) {
742                        sb.append(" inclusive");
743                    } else {
744                        sb.append(" exclusive");
745                    }
746                }
747                sb.append(" to ");
748                if (upperMember == null) {
749                    sb.append("null");
750                } else {
751                    sb.append(upperMember);
752                    if (upperInclusive) {
753                        sb.append(" inclusive");
754                    } else {
755                        sb.append(" exclusive");
756                    }
757                }
758                sb.append(")");
759                return sb.toString();
760            }
761    
762            public void accept(CellRegionVisitor visitor) {
763                visitor.visit(this);
764            }
765    
766            public boolean getLowerInclusive() {
767                return lowerInclusive;
768            }
769    
770            public RolapMember getLowerBound() {
771                return lowerMember;
772            }
773    
774            public boolean getUpperInclusive() {
775                return upperInclusive;
776            }
777    
778            public RolapMember getUpperBound() {
779                return upperMember;
780            }
781        }
782    
783        /**
784         * Cell region formed by a cartesian product of two or more CellRegions.
785         */
786        static class CrossjoinCellRegion implements CellRegionImpl {
787            final List<Dimension> dimensions;
788            private List<CellRegionImpl> components =
789                new ArrayList<CellRegionImpl>();
790    
791            CrossjoinCellRegion(List<CellRegionImpl> regions) {
792                final List<Dimension> dimensionality = new ArrayList<Dimension>();
793                compute(regions, components, dimensionality);
794                dimensions = Collections.unmodifiableList(dimensionality);
795            }
796    
797            private static void compute(
798                List<CellRegionImpl> regions,
799                List<CellRegionImpl> components,
800                List<Dimension> dimensionality)
801            {
802                final Set<Dimension> dimensionSet = new HashSet<Dimension>();
803                for (CellRegionImpl region : regions) {
804                    addComponents(region, components);
805    
806                    final List<Dimension> regionDimensionality =
807                        region.getDimensionality();
808                    dimensionality.addAll(regionDimensionality);
809                    dimensionSet.addAll(regionDimensionality);
810                    assert dimensionSet.size() == dimensionality.size() :
811                        "dimensions in common";
812                }
813            }
814    
815            public void accept(CellRegionVisitor visitor) {
816                visitor.visit(this);
817                for (CellRegion component : components) {
818                    CellRegionImpl cellRegion = (CellRegionImpl) component;
819                    cellRegion.accept(visitor);
820                }
821            }
822    
823            private static void addComponents(
824                CellRegionImpl region,
825                List<CellRegionImpl> list)
826            {
827                if (region instanceof CrossjoinCellRegion) {
828                    CrossjoinCellRegion crossjoinRegion =
829                        (CrossjoinCellRegion) region;
830                    for (CellRegionImpl component : crossjoinRegion.components) {
831                        list.add(component);
832                    }
833                } else {
834                    list.add(region);
835                }
836            }
837    
838            public List<Dimension> getDimensionality() {
839                return dimensions;
840            }
841    
842            public String toString() {
843                return Util.commaList("Crossjoin", components);
844            }
845    
846            public List<CellRegion> getComponents() {
847                return Util.cast(components);
848            }
849        }
850    
851        private static class UnionCellRegion implements CellRegionImpl {
852            private final List<CellRegionImpl> regions;
853    
854            UnionCellRegion(List<CellRegionImpl> regions) {
855                this.regions = regions;
856                assert regions.size() >= 1;
857    
858                // All regions must have same dimensionality.  
859                for (int i = 1; i < regions.size(); i++) {
860                    final CellRegion region0 = regions.get(0);
861                    final CellRegion region = regions.get(i);
862                    assert region0.getDimensionality().equals(
863                        region.getDimensionality());
864                }
865            }
866    
867            public List<Dimension> getDimensionality() {
868                return regions.get(0).getDimensionality();
869            }
870    
871            public String toString() {
872                return Util.commaList("Union", regions);
873            }
874    
875            public void accept(CellRegionVisitor visitor) {
876                visitor.visit(this);
877                for (CellRegionImpl cellRegion : regions) {
878                    cellRegion.accept(visitor);
879                }
880            }
881        }
882    
883        interface CellRegionImpl extends CellRegion {
884            void accept(CellRegionVisitor visitor);
885        }
886    
887        /**
888         * Visitor which visits various sub-types of {@link CellRegion}.
889         */
890        interface CellRegionVisitor {
891            void visit(MemberCellRegion region);
892            void visit(MemberRangeCellRegion region);
893            void visit(UnionCellRegion region);
894            void visit(CrossjoinCellRegion region);
895        }
896    
897        private static class FoundOne extends RuntimeException {
898            private final transient UnionCellRegion region;
899    
900            public FoundOne(UnionCellRegion region) {
901                this.region = region;
902            }
903        }
904    
905        /**
906         * Default implementation of {@link CellRegionVisitor}.
907         */
908        private static class CellRegionVisitorImpl implements CellRegionVisitor {
909            public void visit(MemberCellRegion region) {
910                // nothing
911            }
912    
913            public void visit(MemberRangeCellRegion region) {
914                // nothing
915            }
916    
917            public void visit(UnionCellRegion region) {
918                // nothing
919            }
920    
921            public void visit(CrossjoinCellRegion region) {
922                // nothing
923            }
924        }
925    
926    
927        // ~ member cache control implementation ----------------------------------
928    
929        /**
930         * Implementation-specific extensions to the
931         * {@link mondrian.olap.CacheControl.MemberEditCommand} interface.
932         */
933        interface MemberEditCommandPlus extends MemberEditCommand {
934            /**
935             * Executes this command, and gathers a list of cell regions affected
936             * in the {@code cellRegionList} parameter. The caller will flush the
937             * cell regions later.
938             *
939             * @param cellRegionList Populated with a list of cell regions which
940             * are invalidated by this action
941             */
942            void execute(final List<CellRegion> cellRegionList);
943        }
944    
945        /**
946         * Implementation-specific extensions to the
947         * {@link mondrian.olap.CacheControl.MemberSet} interface.
948         */
949        interface MemberSetPlus extends MemberSet {
950            /**
951             * Accepts a visitor.
952             *
953             * @param visitor Visitor
954             */
955            void accept(MemberSetVisitor visitor);
956    
957            /**
958             * Filters this member set, returning a member set containing all
959             * members at a given Level. When applicable, returns this member set
960             * unchanged.
961             *
962             * @param level Level
963             * @return Member set with members not at the given level removed
964             */
965            MemberSetPlus filter(RolapLevel level);
966        }
967    
968        /**
969         * Visits the subclasses of {@link MemberSetPlus}.
970         */
971        interface MemberSetVisitor {
972            void visit(SimpleMemberSet s);
973            void visit(UnionMemberSet s);
974            void visit(RangeMemberSet s);
975        }
976    
977        /**
978         * Default implementation of {@link MemberSetVisitor}.
979         *
980         * <p>The default implementation may not be efficient. For example, if
981         * flushing a range of members from the cache, you may not wish to fetch
982         * all of the members into the cache in order to flush them.
983         */
984        public static abstract class MemberSetVisitorImpl
985            implements MemberSetVisitor
986        {
987            public void visit(UnionMemberSet s) {
988                for (MemberSetPlus item : s.items) {
989                    item.accept(this);
990                }
991            }
992    
993            public void visit(RangeMemberSet s) {
994                final MemberReader memberReader =
995                    s.level.getHierarchy().getMemberReader();
996                visitRange(
997                    memberReader, s.level, s.lowerMember, s.upperMember,
998                    s.descendants);
999            }
1000    
1001            protected void visitRange(
1002                MemberReader memberReader,
1003                RolapLevel level,
1004                RolapMember lowerMember,
1005                RolapMember upperMember,
1006                boolean recurse)
1007            {
1008                final List<RolapMember> list = new ArrayList<RolapMember>();
1009                memberReader.getMemberRange(level, lowerMember, upperMember, list);
1010                for (RolapMember member : list) {
1011                    visit(member);
1012                }
1013                if (recurse) {
1014                    list.clear();
1015                    memberReader.getMemberChildren(lowerMember, list);
1016                    if (list.isEmpty()) {
1017                        return;
1018                    }
1019                    RolapMember lowerChild = list.get(0);
1020                    list.clear();
1021                    memberReader.getMemberChildren(upperMember, list);
1022                    if (list.isEmpty()) {
1023                        return;
1024                    }
1025                    RolapMember upperChild = list.get(list.size() - 1);
1026                    visitRange(
1027                        memberReader, level, lowerChild, upperChild, recurse);
1028                }
1029            }
1030    
1031            public void visit(SimpleMemberSet s) {
1032                for (RolapMember member : s.members) {
1033                    visit(member);
1034                }
1035            }
1036    
1037            /**
1038             * Visits a single member.
1039             *
1040             * @param member Member
1041             */
1042            public abstract void visit(RolapMember member);
1043        }
1044    
1045        /**
1046         * Member set containing no members.
1047         */
1048        static class EmptyMemberSet implements MemberSetPlus {
1049            public static final EmptyMemberSet INSTANCE = new EmptyMemberSet();
1050    
1051            private EmptyMemberSet() {
1052                // prevent instantiation except for singleton
1053            }
1054    
1055            public void accept(MemberSetVisitor visitor) {
1056                // nothing
1057            }
1058    
1059            public MemberSetPlus filter(RolapLevel level) {
1060                return this;
1061            }
1062    
1063            public String toString() {
1064                return "Empty";
1065            }
1066        }
1067    
1068        /**
1069         * Member set defined by a list of members from one hierarchy.
1070         */
1071        static class SimpleMemberSet implements MemberSetPlus {
1072            public final List<RolapMember> members;
1073            public final boolean descendants; // the set includes the descendants of all members
1074            public final RolapHierarchy hierarchy;
1075    
1076            SimpleMemberSet(List<RolapMember> members, boolean descendants) {
1077                this.members = new ArrayList<RolapMember>(members);
1078                stripMemberList(this.members);
1079                this.descendants = descendants;
1080                this.hierarchy =
1081                    members.isEmpty()
1082                        ? null
1083                        : members.get(0).getHierarchy();
1084            }
1085    
1086            public String toString() {
1087                return Util.commaList("Member", members);
1088            }
1089    
1090            public void accept(MemberSetVisitor visitor) {
1091                // Don't descend the subtrees here: may not want to load them into
1092                // cache.
1093                visitor.visit(this);
1094            }
1095    
1096            public MemberSetPlus filter(RolapLevel level) {
1097                List<RolapMember> filteredMembers = new ArrayList<RolapMember>();
1098                for (RolapMember member : members) {
1099                    if (member.getLevel().equals(level)) {
1100                        filteredMembers.add(member);
1101                    }
1102                }
1103                if (filteredMembers.isEmpty()) {
1104                    return EmptyMemberSet.INSTANCE;
1105                } else if (filteredMembers.equals(members)) {
1106                    return this;
1107                } else {
1108                    return new SimpleMemberSet(filteredMembers, false);
1109                }
1110            }
1111        }
1112    
1113        /**
1114         * Member set defined by the union of other member sets.
1115         */
1116        static class UnionMemberSet implements MemberSetPlus {
1117            private final List<MemberSetPlus> items;
1118    
1119            UnionMemberSet(List<MemberSetPlus> items) {
1120                this.items = items;
1121            }
1122    
1123            public String toString() {
1124                final StringBuilder sb = new StringBuilder("Union(");
1125                for (int i = 0; i < items.size(); i++) {
1126                    if (i > 0) {
1127                        sb.append(", ");
1128                    }
1129                    MemberSetPlus item = items.get(i);
1130                    sb.append(item.toString());
1131                }
1132                sb.append(")");
1133                return sb.toString();
1134            }
1135    
1136            public void accept(MemberSetVisitor visitor) {
1137                visitor.visit(this);
1138            }
1139    
1140            public MemberSetPlus filter(RolapLevel level) {
1141                final List<MemberSetPlus> filteredItems =
1142                    new ArrayList<MemberSetPlus>();
1143                for (MemberSetPlus item : items) {
1144                    final MemberSetPlus filteredItem = item.filter(level);
1145                    if (filteredItem == EmptyMemberSet.INSTANCE) {
1146                        // skip it
1147                    } else {
1148                        assert !(filteredItem instanceof EmptyMemberSet);
1149                        filteredItems.add(filteredItem);
1150                    }
1151                }
1152                if (filteredItems.isEmpty()) {
1153                    return EmptyMemberSet.INSTANCE;
1154                } else if (filteredItems.equals(items)) {
1155                    return this;
1156                } else {
1157                    return new UnionMemberSet(filteredItems);
1158                }
1159            }
1160        }
1161    
1162        /**
1163         * Member set defined by a range of members between a lower and upper
1164         * bound.
1165         */
1166        static class RangeMemberSet implements MemberSetPlus {
1167            private final RolapMember lowerMember;
1168            private final boolean lowerInclusive;
1169            private final RolapMember upperMember;
1170            private final boolean upperInclusive;
1171            private final boolean descendants;
1172            private final RolapLevel level;
1173    
1174            RangeMemberSet(
1175                RolapMember lowerMember,
1176                boolean lowerInclusive,
1177                RolapMember upperMember,
1178                boolean upperInclusive,
1179                boolean descendants)
1180            {
1181                assert lowerMember != null || upperMember != null;
1182                assert lowerMember == null
1183                    || upperMember == null
1184                    || lowerMember.getLevel() == upperMember.getLevel();
1185                assert !(lowerMember == null && lowerInclusive);
1186                assert !(upperMember == null && upperInclusive);
1187                assert !(lowerMember instanceof RolapCubeMember);
1188                assert !(upperMember instanceof RolapCubeMember);
1189                this.lowerMember = lowerMember;
1190                this.lowerInclusive = lowerInclusive;
1191                this.upperMember = upperMember;
1192                this.upperInclusive = upperInclusive;
1193                this.descendants = descendants;
1194                this.level = lowerMember == null ?
1195                    upperMember.getLevel() :
1196                    lowerMember.getLevel();
1197            }
1198    
1199            public String toString() {
1200                final StringBuilder sb = new StringBuilder("Range(");
1201                if (lowerMember == null) {
1202                    sb.append("null");
1203                } else {
1204                    sb.append(lowerMember);
1205                    if (lowerInclusive) {
1206                        sb.append(" inclusive");
1207                    } else {
1208                        sb.append(" exclusive");
1209                    }
1210                }
1211                sb.append(" to ");
1212                if (upperMember == null) {
1213                    sb.append("null");
1214                } else {
1215                    sb.append(upperMember);
1216                    if (upperInclusive) {
1217                        sb.append(" inclusive");
1218                    } else {
1219                        sb.append(" exclusive");
1220                    }
1221                }
1222                sb.append(")");
1223                return sb.toString();
1224            }
1225    
1226            public void accept(MemberSetVisitor visitor) {
1227                // Don't traverse the range here: may not want to load it into cache
1228                visitor.visit(this);
1229            }
1230    
1231            public MemberSetPlus filter(RolapLevel level) {
1232                if (level == this.level) {
1233                    return this;
1234                } else {
1235                    return filter2(level, this.level, lowerMember, upperMember);
1236                }
1237            }
1238    
1239            public MemberSetPlus filter2(
1240                RolapLevel seekLevel,
1241                RolapLevel level,
1242                RolapMember lower,
1243                RolapMember upper)
1244            {
1245                if (level == seekLevel) {
1246                    return new RangeMemberSet(
1247                        lower, lowerInclusive, upper, upperInclusive, false);
1248                } else if (descendants
1249                    && level.getHierarchy() == seekLevel.getHierarchy()
1250                    && level.getDepth() < seekLevel.getDepth())
1251                {
1252                    final MemberReader memberReader =
1253                        level.getHierarchy().getMemberReader();
1254                    final List<RolapMember> list = new ArrayList<RolapMember>();
1255                    memberReader.getMemberChildren(lower, list);
1256                    if (list.isEmpty()) {
1257                        return EmptyMemberSet.INSTANCE;
1258                    }
1259                    RolapMember lowerChild = list.get(0);
1260                    list.clear();
1261                    memberReader.getMemberChildren(upper, list);
1262                    if (list.isEmpty()) {
1263                        return EmptyMemberSet.INSTANCE;
1264                    }
1265                    RolapMember upperChild = list.get(list.size() - 1);
1266                    return filter2(
1267                        seekLevel, (RolapLevel) level.getChildLevel(),
1268                        lowerChild, upperChild);
1269                } else {
1270                    return EmptyMemberSet.INSTANCE;
1271                }
1272            }
1273        }
1274    
1275        /**
1276         * Command consisting of a set of commands executed in sequence.
1277         */
1278        private static class CompoundCommand implements MemberEditCommandPlus {
1279            private final List<MemberEditCommandPlus> commandList;
1280    
1281            CompoundCommand(List<MemberEditCommandPlus> commandList) {
1282                this.commandList = commandList;
1283            }
1284    
1285            public String toString() {
1286                return Util.commaList("Compound", commandList);
1287            }
1288    
1289            public void execute(final List<CellRegion> cellRegionList) {
1290                for (MemberEditCommandPlus command : commandList) {
1291                    command.execute(cellRegionList);
1292                }
1293            }
1294        }
1295    
1296        /**
1297         * Command that deletes a member and its descendants from the cache.
1298         */
1299        private class DeleteMemberCommand
1300            extends MemberSetVisitorImpl
1301            implements MemberEditCommandPlus
1302        {
1303            private final MemberSetPlus set;
1304            private List<CellRegion> cellRegionList;
1305    
1306            DeleteMemberCommand(MemberSetPlus set) {
1307                this.set = set;
1308            }
1309            
1310            public String toString() {
1311                return "DeleteMemberCommand(" + set + ")";
1312            }
1313    
1314            public void execute(final List<CellRegion> cellRegionList) {
1315                // NOTE: use of member makes this class non-reentrant
1316                this.cellRegionList = cellRegionList;
1317                set.accept(this);
1318                this.cellRegionList = null;
1319            }
1320    
1321            public void visit(RolapMember member) {
1322                deleteMember(member, member.getParentMember(), cellRegionList);
1323            }
1324        }
1325    
1326        /**
1327         * Command that adds a new member to the cache.
1328         */
1329        private class AddMemberCommand implements MemberEditCommandPlus {
1330            private final RolapMember member;
1331    
1332            public AddMemberCommand(RolapMember member) {
1333                assert member != null;
1334                this.member = stripMember(member);
1335            }
1336    
1337            public String toString() {
1338                return "AddMemberCommand(" + member + ")";
1339            }
1340            
1341            public void execute(List<CellRegion> cellRegionList) {
1342                addMember(member, member.getParentMember(), cellRegionList);
1343            }
1344        }
1345    
1346        /**
1347         * Command that moves a member to a new parent.
1348         */
1349        private class MoveMemberCommand implements MemberEditCommandPlus {
1350            private final RolapMember member;
1351            private final RolapMember newParent;
1352    
1353            MoveMemberCommand(RolapMember member, RolapMember newParent) {
1354                this.member = member;
1355                this.newParent = newParent;
1356            }
1357            
1358            public String toString() {
1359                return "MoveMemberCommand(" + member + ", " + newParent + ")";
1360            }
1361            
1362            public void execute(final List<CellRegion> cellRegionList) {
1363                deleteMember(member, member.getParentMember(), cellRegionList);
1364                member.setParentMember(newParent);
1365                addMember(member, member.getParentMember(), cellRegionList);
1366            }
1367        }
1368    
1369        /**
1370         * Command that changes one or more properties of a member.
1371         */
1372        private class ChangeMemberPropsCommand
1373            extends MemberSetVisitorImpl
1374            implements MemberEditCommandPlus
1375        {
1376            final MemberSetPlus memberSet;
1377            final Map<String, Object> propertyValues;
1378    
1379            ChangeMemberPropsCommand(
1380                MemberSetPlus memberSet,
1381                Map<String, Object> propertyValues)
1382            {
1383                this.memberSet = memberSet;
1384                this.propertyValues = propertyValues;
1385            }
1386            
1387            public String toString() {
1388                return "CreateMemberPropsCommand(" + memberSet
1389                    + ", " + propertyValues + ")";
1390            }
1391    
1392            public void execute(List<CellRegion> cellRegionList) {
1393                // ignore cellRegionList - no changes to cell cache
1394                memberSet.accept(this);
1395            }
1396    
1397            public void visit(RolapMember member) {
1398                // Change member's properties.
1399                member = stripMember(member);
1400                final MemberCache memberCache = getMemberCache(member);
1401                final Object cacheKey =
1402                    memberCache.makeKey(
1403                        member.getParentMember(),
1404                        member.getKey());
1405                final RolapMember cacheMember = memberCache.getMember(cacheKey);
1406                if (cacheMember == null) {
1407                    return;
1408                }
1409                for (Map.Entry<String, Object> entry : propertyValues.entrySet()) {
1410                    cacheMember.setProperty(entry.getKey(), entry.getValue());
1411                }
1412            }
1413        }
1414    
1415        private static RolapMember stripMember(RolapMember member) {
1416            if (member instanceof RolapCubeMember) {
1417                member = ((RolapCubeMember) member).rolapMember;
1418            }
1419            return member;
1420        }
1421    
1422        private static void stripMemberList(List<RolapMember> members) {
1423            for (int i = 0; i < members.size(); i++) {
1424                RolapMember member = members.get(i);