001    /*
002    // $Id: //open/mondrian/src/main/mondrian/rolap/CacheControlImpl.java#11 $
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/src/main/mondrian/rolap/CacheControlImpl.java#11 $
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 List<Member> measures =
134                cube.getSchemaReader(null).getLevelMembers(
135                    measuresDimension.getHierarchy().getLevels()[0],
136                    false);
137            return new MemberCellRegion(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 measure : measuresList) {
396                if (measure instanceof RolapStoredMeasure) {
397                    RolapStoredMeasure storedMeasure = (RolapStoredMeasure) measure;
398                    final RolapStar.Measure starMeasure =
399                        (RolapStar.Measure) storedMeasure.getStarMeasure();
400                    if (!starList.contains(starMeasure.getStar())) {
401                        starList.add(starMeasure.getStar());
402                    }
403                }
404            }
405            return starList;
406        }
407    
408        public void printCacheState(
409            PrintWriter pw,
410            CellRegion region)
411        {
412            List<RolapStar> starList = getStarList(region);
413            for (RolapStar star : starList) {
414                star.print(pw, "", false);
415            }
416        }
417    
418        public MemberSet createMemberSet(Member member, boolean descendants)
419        {
420            return new SimpleMemberSet(
421                Collections.singletonList((RolapMember) member),
422                descendants);
423        }
424    
425        public MemberSet createMemberSet(
426            boolean lowerInclusive,
427            Member lowerMember,
428            boolean upperInclusive,
429            Member upperMember,
430            boolean descendants)
431        {
432            // TODO check that upperMember & lowerMember are in same Level
433            if (lowerMember == null) {
434                lowerInclusive = false;
435            }
436            if (upperMember == null) {
437                upperInclusive = false;
438            }
439            return new RangeMemberSet(
440                stripMember((RolapMember) lowerMember), lowerInclusive,
441                stripMember((RolapMember) upperMember), upperInclusive,
442                descendants);
443        }
444    
445        public MemberSet createUnionSet(MemberSet... args)
446        {
447            //noinspection unchecked
448            return new UnionMemberSet((List) Arrays.asList(args));
449        }
450    
451        public MemberSet filter(Level level, MemberSet baseSet) {
452            if (level instanceof RolapCubeLevel) {
453                // be forgiving
454                level = ((RolapCubeLevel) level).getRolapLevel();
455            }
456            return ((MemberSetPlus) baseSet).filter((RolapLevel) level);
457        }
458    
459        public void flush(MemberSet memberSet) {
460            // REVIEW How is flush(s) different to executing createDeleteCommand(s) ?
461            synchronized (MEMBER_CACHE_LOCK) {
462                final List<CellRegion> cellRegionList = new ArrayList<CellRegion>();
463                ((MemberSetPlus) memberSet).accept(
464                    new MemberSetVisitorImpl() {
465                        public void visit(RolapMember member) {
466                            flushMember(member, cellRegionList);
467                        }
468                    }
469               );
470                // STUB: flush the set: another visitor
471    
472                // finally, flush cells now invalid
473                flushRegionList(cellRegionList);
474            }
475        }
476    
477        public void printCacheState(PrintWriter pw, MemberSet set)
478        {
479            synchronized (MEMBER_CACHE_LOCK) {
480                pw.println("need to implement printCacheState"); // TODO:
481            }
482        }
483    
484        public MemberEditCommand createCompoundCommand(
485            List<MemberEditCommand> commandList)
486        {
487            //noinspection unchecked
488            return new CompoundCommand((List) commandList);
489        }
490    
491        public MemberEditCommand createCompoundCommand(
492            MemberEditCommand... commands)
493        {
494            //noinspection unchecked
495            return new CompoundCommand((List) Arrays.asList(commands));
496        }
497    
498        public MemberEditCommand createDeleteCommand(Member member) {
499            if (member == null) {
500                throw new IllegalArgumentException("cannot delete null member");
501            }
502            if (((RolapLevel) member.getLevel()).isParentChild()) {
503                throw new IllegalArgumentException(
504                    "delete member not supported for parent-child hierarchy");
505            }
506            return createDeleteCommand(createMemberSet(member, false));
507        }
508    
509        public MemberEditCommand createDeleteCommand(MemberSet s) {
510            return new DeleteMemberCommand((MemberSetPlus) s);
511        }
512    
513        public MemberEditCommand createAddCommand(
514            Member member) throws IllegalArgumentException
515        {
516            if (member == null) {
517                throw new IllegalArgumentException("cannot add null member");
518            }
519            if (((RolapLevel) member.getLevel()).isParentChild()) {
520                throw new IllegalArgumentException(
521                    "add member not supported for parent-child hierarchy");
522            }
523            return new AddMemberCommand((RolapMember) member);
524        }
525    
526        public MemberEditCommand createMoveCommand(Member member, Member loc)
527            throws IllegalArgumentException
528        {
529            if (member == null) {
530                throw new IllegalArgumentException("cannot move null member");
531            }
532            if (((RolapLevel) member.getLevel()).isParentChild()) {
533                throw new IllegalArgumentException(
534                    "move member not supported for parent-child hierarchy");
535            }
536            if (loc == null) {
537                throw new IllegalArgumentException("cannot move member to null location");
538            }
539            // TODO: check that MEMBER and LOC (its new parent) have appropriate Levels
540            return new MoveMemberCommand((RolapMember) member, (RolapMember) loc);
541        }
542    
543        public MemberEditCommand createSetPropertyCommand(
544            Member member,
545            String name,
546            Object value)
547            throws IllegalArgumentException
548        {
549            if (member == null) {
550                throw new IllegalArgumentException("cannot set properties on null member");
551            }
552            if (((RolapLevel) member.getLevel()).isParentChild()) {
553                throw new IllegalArgumentException(
554                    "set properties not supported for parent-child hierarchy");
555            }
556            // TODO: validate that prop NAME exists for Level of MEMBER
557            return new ChangeMemberPropsCommand(
558                new SimpleMemberSet(
559                    Collections.singletonList((RolapMember) member),
560                    false),
561                Collections.singletonMap(name, value));
562        }
563    
564        public MemberEditCommand createSetPropertyCommand(
565            MemberSet members,
566            Map<String, Object> propertyValues)
567            throws IllegalArgumentException
568        {
569            // TODO: check that members all at same Level, and validate that props exist
570            validateSameLevel((MemberSetPlus) members);
571            return new ChangeMemberPropsCommand(
572                (MemberSetPlus) members,
573                propertyValues);
574        }
575    
576        /**
577         * Validates that all members of a member set are the same level.
578         *
579         * @param memberSet Member set
580         * @throws IllegalArgumentException if members are from more than one level
581         */
582        private void validateSameLevel(MemberSetPlus memberSet)
583            throws IllegalArgumentException
584        {
585            memberSet.accept(
586                new MemberSetVisitor() {
587                    final Set<RolapLevel> levelSet = new HashSet<RolapLevel>();
588    
589                    private void visitMember(
590                        RolapMember member,
591                        boolean descendants)
592                    {
593                        final String message =
594                            "all members in set must belong to same level";
595                        if (levelSet.add(member.getLevel())
596                            && levelSet.size() > 1) {
597                            throw new IllegalArgumentException(message);
598                        }
599                        if (descendants
600                            && member.getLevel().getChildLevel() != null) {
601                            throw new IllegalArgumentException(message);
602                        }
603                    }
604    
605                    public void visit(SimpleMemberSet simpleMemberSet) {
606                        for (RolapMember member : simpleMemberSet.members) {
607                            visitMember(member, simpleMemberSet.descendants);
608                        }
609                    }
610    
611                    public void visit(UnionMemberSet unionMemberSet) {
612                        for (MemberSetPlus item : unionMemberSet.items) {
613                            item.accept(this);
614                        }
615                    }
616    
617                    public void visit(RangeMemberSet rangeMemberSet) {
618                        visitMember(
619                            rangeMemberSet.lowerMember,
620                            rangeMemberSet.descendants);
621                        visitMember(
622                            rangeMemberSet.upperMember,
623                            rangeMemberSet.descendants);
624                    }
625                }
626           );
627        }
628    
629        public void execute(MemberEditCommand cmd) {
630            final BooleanProperty prop =
631                MondrianProperties.instance().EnableRolapCubeMemberCache;
632            if (prop.get()) {
633                throw new IllegalArgumentException(
634                    "Member cache control operations are not allowed unless "
635                        + "property " + prop.getPath() + " is false");
636            }
637            synchronized (MEMBER_CACHE_LOCK) {
638                final List<CellRegion> cellRegionList =
639                    new ArrayList<CellRegion>();
640                ((MemberEditCommandPlus) cmd).execute(cellRegionList);
641                if (false) {
642                    // TODO: Flushing regions currently fails with the error
643                    // "Region of cells to be flushed must contain measures". This
644                    // method should receive a cube (or cubes) whose measures to
645                    // flush.
646                    flushRegionList(cellRegionList);
647                }
648            }
649        }
650    
651        private static MemberCache getMemberCache(RolapMember member) {
652            final MemberReader memberReader =
653                member.getHierarchy().getMemberReader();
654            SmartMemberReader smartMemberReader =
655                (SmartMemberReader) memberReader;
656            return smartMemberReader.getMemberCache();
657        }
658    
659        // cell cache control implementation
660    
661        /**
662         * Cell region formed by a list of members.
663         *
664         * @see MemberRangeCellRegion
665         */
666        static class MemberCellRegion implements CellRegionImpl {
667            private final List<Member> memberList;
668            private final Dimension dimension;
669    
670            MemberCellRegion(List<Member> memberList, boolean descendants) {
671                assert memberList.size() > 0;
672                this.memberList = memberList;
673                this.dimension = (memberList.get(0)).getDimension();
674                Util.discard(descendants);
675            }
676    
677            public List<Dimension> getDimensionality() {
678                return Collections.singletonList(dimension);
679            }
680    
681            public String toString() {
682                return Util.commaList("Member", memberList);
683            }
684    
685            public void accept(CellRegionVisitor visitor) {
686                visitor.visit(this);
687            }
688    
689            public List<Member> getMemberList() {
690                return memberList;
691            }
692        }
693    
694        /**
695         * Cell region formed a range of members between a lower and upper bound.
696         */
697        static class MemberRangeCellRegion implements CellRegionImpl {
698            private final RolapMember lowerMember;
699            private final boolean lowerInclusive;
700            private final RolapMember upperMember;
701            private final boolean upperInclusive;
702            private final boolean descendants;
703            private final RolapLevel level;
704    
705            MemberRangeCellRegion(
706                RolapMember lowerMember,
707                boolean lowerInclusive,
708                RolapMember upperMember,
709                boolean upperInclusive,
710                boolean descendants)
711            {
712                assert lowerMember != null || upperMember != null;
713                assert lowerMember == null
714                    || upperMember == null
715                    || lowerMember.getLevel() == upperMember.getLevel();
716                assert !(lowerMember == null && lowerInclusive);
717                assert !(upperMember == null && upperInclusive);
718                this.lowerMember = lowerMember;
719                this.lowerInclusive = lowerInclusive;
720                this.upperMember = upperMember;
721                this.upperInclusive = upperInclusive;
722                this.descendants = descendants;
723                this.level = lowerMember == null ?
724                    upperMember.getLevel() :
725                    lowerMember.getLevel();
726            }
727    
728            public List<Dimension> getDimensionality() {
729                return Collections.singletonList(level.getDimension());
730            }
731    
732            public RolapLevel getLevel() {
733                return level;
734            }
735    
736            public String toString() {
737                final StringBuilder sb = new StringBuilder("Range(");
738                if (lowerMember == null) {
739                    sb.append("null");
740                } else {
741                    sb.append(lowerMember);
742                    if (lowerInclusive) {
743                        sb.append(" inclusive");
744                    } else {
745                        sb.append(" exclusive");
746                    }
747                }
748                sb.append(" to ");
749                if (upperMember == null) {
750                    sb.append("null");
751                } else {
752                    sb.append(upperMember);
753                    if (upperInclusive) {
754                        sb.append(" inclusive");
755                    } else {
756                        sb.append(" exclusive");
757                    }
758                }
759                sb.append(")");
760                return sb.toString();
761            }
762    
763            public void accept(CellRegionVisitor visitor) {
764                visitor.visit(this);
765            }
766    
767            public boolean getLowerInclusive() {
768                return lowerInclusive;
769            }
770    
771            public RolapMember getLowerBound() {
772                return lowerMember;
773            }
774    
775            public boolean getUpperInclusive() {
776                return upperInclusive;
777            }
778    
779            public RolapMember getUpperBound() {
780                return upperMember;
781            }
782        }
783    
784        /**
785         * Cell region formed by a cartesian product of two or more CellRegions.
786         */
787        static class CrossjoinCellRegion implements CellRegionImpl {
788            final List<Dimension> dimensions;
789            private List<CellRegionImpl> components =
790                new ArrayList<CellRegionImpl>();
791    
792            CrossjoinCellRegion(List<CellRegionImpl> regions) {
793                final List<Dimension> dimensionality = new ArrayList<Dimension>();
794                compute(regions, components, dimensionality);
795                dimensions = Collections.unmodifiableList(dimensionality);
796            }
797    
798            private static void compute(
799                List<CellRegionImpl> regions,
800                List<CellRegionImpl> components,
801                List<Dimension> dimensionality)
802            {
803                final Set<Dimension> dimensionSet = new HashSet<Dimension>();
804                for (CellRegionImpl region : regions) {
805                    addComponents(region, components);
806    
807                    final List<Dimension> regionDimensionality =
808                        region.getDimensionality();
809                    dimensionality.addAll(regionDimensionality);
810                    dimensionSet.addAll(regionDimensionality);
811                    assert dimensionSet.size() == dimensionality.size() :
812                        "dimensions in common";
813                }
814            }
815    
816            public void accept(CellRegionVisitor visitor) {
817                visitor.visit(this);
818                for (CellRegion component : components) {
819                    CellRegionImpl cellRegion = (CellRegionImpl) component;
820                    cellRegion.accept(visitor);
821                }
822            }
823    
824            private static void addComponents(
825                CellRegionImpl region,
826                List<CellRegionImpl> list)
827            {
828                if (region instanceof CrossjoinCellRegion) {
829                    CrossjoinCellRegion crossjoinRegion =
830                        (CrossjoinCellRegion) region;
831                    for (CellRegionImpl component : crossjoinRegion.components) {
832                        list.add(component);
833                    }
834                } else {
835                    list.add(region);
836                }
837            }
838    
839            public List<Dimension> getDimensionality() {
840                return dimensions;
841            }
842    
843            public String toString() {
844                return Util.commaList("Crossjoin", components);
845            }
846    
847            public List<CellRegion> getComponents() {
848                return Util.cast(components);
849            }
850        }
851    
852        private static class UnionCellRegion implements CellRegionImpl {
853            private final List<CellRegionImpl> regions;
854    
855            UnionCellRegion(List<CellRegionImpl> regions) {
856                this.regions = regions;
857                assert regions.size() >= 1;
858    
859                // All regions must have same dimensionality.
860                for (int i = 1; i < regions.size(); i++) {
861                    final CellRegion region0 = regions.get(0);
862                    final CellRegion region = regions.get(i);
863                    assert region0.getDimensionality().equals(
864                        region.getDimensionality());
865                }
866            }
867    
868            public List<Dimension> getDimensionality() {
869                return regions.get(0).getDimensionality();
870            }
871    
872            public String toString() {
873                return Util.commaList("Union", regions);
874            }
875    
876            public void accept(CellRegionVisitor visitor) {
877                visitor.visit(this);
878                for (CellRegionImpl cellRegion : regions) {
879                    cellRegion.accept(visitor);
880                }
881            }
882        }
883    
884        interface CellRegionImpl extends CellRegion {
885            void accept(CellRegionVisitor visitor);
886        }
887    
888        /**
889         * Visitor which visits various sub-types of {@link CellRegion}.
890         */
891        interface CellRegionVisitor {
892            void visit(MemberCellRegion region);
893            void visit(MemberRangeCellRegion region);
894            void visit(UnionCellRegion region);
895            void visit(CrossjoinCellRegion region);
896        }
897    
898        private static class FoundOne extends RuntimeException {
899            private final transient UnionCellRegion region;
900    
901            public FoundOne(UnionCellRegion region) {
902                this.region = region;
903            }
904        }
905    
906        /**
907         * Default implementation of {@link CellRegionVisitor}.
908         */
909        private static class CellRegionVisitorImpl implements CellRegionVisitor {
910            public void visit(MemberCellRegion region) {
911                // nothing
912            }
913    
914            public void visit(MemberRangeCellRegion region) {
915                // nothing
916            }
917    
918            public void visit(UnionCellRegion region) {
919                // nothing
920            }
921    
922            public void visit(CrossjoinCellRegion region) {
923                // nothing
924            }
925        }
926    
927    
928        // ~ member cache control implementation ----------------------------------
929    
930        /**
931         * Implementation-specific extensions to the
932         * {@link mondrian.olap.CacheControl.MemberEditCommand} interface.
933         */
934        interface MemberEditCommandPlus extends MemberEditCommand {
935            /**
936             * Executes this command, and gathers a list of cell regions affected
937             * in the {@code cellRegionList} parameter. The caller will flush the
938             * cell regions later.
939             *
940             * @param cellRegionList Populated with a list of cell regions which
941             * are invalidated by this action
942             */
943            void execute(final List<CellRegion> cellRegionList);
944        }
945    
946        /**
947         * Implementation-specific extensions to the
948         * {@link mondrian.olap.CacheControl.MemberSet} interface.
949         */
950        interface MemberSetPlus extends MemberSet {
951            /**
952             * Accepts a visitor.
953             *
954             * @param visitor Visitor
955             */
956            void accept(MemberSetVisitor visitor);
957    
958            /**
959             * Filters this member set, returning a member set containing all
960             * members at a given Level. When applicable, returns this member set
961             * unchanged.
962             *
963             * @param level Level
964             * @return Member set with members not at the given level removed
965             */
966            MemberSetPlus filter(RolapLevel level);
967        }
968    
969        /**
970         * Visits the subclasses of {@link MemberSetPlus}.
971         */
972        interface MemberSetVisitor {
973            void visit(SimpleMemberSet s);
974            void visit(UnionMemberSet s);
975            void visit(RangeMemberSet s);
976        }
977    
978        /**
979         * Default implementation of {@link MemberSetVisitor}.
980         *
981         * <p>The default implementation may not be efficient. For example, if
982         * flushing a range of members from the cache, you may not wish to fetch
983         * all of the members into the cache in order to flush them.
984         */
985        public static abstract class MemberSetVisitorImpl
986            implements MemberSetVisitor
987        {
988            public void visit(UnionMemberSet s) {
989                for (MemberSetPlus item : s.items) {
990                    item.accept(this);
991                }
992            }
993    
994            public void visit(RangeMemberSet s) {
995                final MemberReader memberReader =
996                    s.level.getHierarchy().getMemberReader();
997                visitRange(
998                    memberReader, s.level, s.lowerMember, s.upperMember,
999                    s.descendants);
1000            }
1001    
1002            protected void visitRange(
1003                MemberReader memberReader,
1004                RolapLevel level,
1005                RolapMember lowerMember,
1006                RolapMember upperMember,
1007                boolean recurse)
1008            {
1009                final List<RolapMember> list = new ArrayList<RolapMember>();
1010                memberReader.getMemberRange(level, lowerMember, upperMember, list);
1011                for (RolapMember member : list) {
1012                    visit(member);
1013                }
1014                if (recurse) {
1015                    list.clear();
1016                    memberReader.getMemberChildren(lowerMember, list);
1017                    if (list.isEmpty()) {
1018                        return;
1019                    }
1020                    RolapMember lowerChild = list.get(0);
1021                    list.clear();
1022                    memberReader.getMemberChildren(upperMember, list);
1023                    if (list.isEmpty()) {
1024                        return;
1025                    }
1026                    RolapMember upperChild = list.get(list.size() - 1);
1027                    visitRange(
1028                        memberReader, level, lowerChild, upperChild, recurse);
1029                }
1030            }
1031    
1032            public void visit(SimpleMemberSet s) {
1033                for (RolapMember member : s.members) {
1034                    visit(member);
1035                }
1036            }
1037    
1038            /**
1039             * Visits a single member.
1040             *
1041             * @param member Member
1042             */
1043            public abstract void visit(RolapMember member);
1044        }
1045    
1046        /**
1047         * Member set containing no members.
1048         */
1049        static class EmptyMemberSet implements MemberSetPlus {
1050            public static final EmptyMemberSet INSTANCE = new EmptyMemberSet();
1051    
1052            private EmptyMemberSet() {
1053                // prevent instantiation except for singleton
1054            }
1055    
1056            public void accept(MemberSetVisitor visitor) {
1057                // nothing
1058            }
1059    
1060            public MemberSetPlus filter(RolapLevel level) {
1061                return this;
1062            }
1063    
1064            public String toString() {
1065                return "Empty";
1066            }
1067        }
1068    
1069        /**
1070         * Member set defined by a list of members from one hierarchy.
1071         */
1072        static class SimpleMemberSet implements MemberSetPlus {
1073            public final List<RolapMember> members;
1074            public final boolean descendants; // the set includes the descendants of all members
1075            public final RolapHierarchy hierarchy;
1076    
1077            SimpleMemberSet(List<RolapMember> members, boolean descendants) {
1078                this.members = new ArrayList<RolapMember>(members);
1079                stripMemberList(this.members);
1080                this.descendants = descendants;
1081                this.hierarchy =
1082                    members.isEmpty()
1083                        ? null
1084                        : members.get(0).getHierarchy();
1085            }
1086    
1087            public String toString() {
1088                return Util.commaList("Member", members);
1089            }
1090    
1091            public void accept(MemberSetVisitor visitor) {
1092                // Don't descend the subtrees here: may not want to load them into
1093                // cache.
1094                visitor.visit(this);
1095            }
1096    
1097            public MemberSetPlus filter(RolapLevel level) {
1098                List<RolapMember> filteredMembers = new ArrayList<RolapMember>();
1099                for (RolapMember member : members) {
1100                    if (member.getLevel().equals(level)) {
1101                        filteredMembers.add(member);
1102                    }
1103                }
1104                if (filteredMembers.isEmpty()) {
1105                    return EmptyMemberSet.INSTANCE;
1106                } else if (filteredMembers.equals(members)) {
1107                    return this;
1108                } else {
1109                    return new SimpleMemberSet(filteredMembers, false);
1110                }
1111            }
1112        }
1113    
1114        /**
1115         * Member set defined by the union of other member sets.
1116         */
1117        static class UnionMemberSet implements MemberSetPlus {
1118            private final List<MemberSetPlus> items;
1119    
1120            UnionMemberSet(List<MemberSetPlus> items) {
1121                this.items = items;
1122            }
1123    
1124            public String toString() {
1125                final StringBuilder sb = new StringBuilder("Union(");
1126                for (int i = 0; i < items.size(); i++) {
1127                    if (i > 0) {
1128                        sb.append(", ");
1129                    }
1130                    MemberSetPlus item = items.get(i);
1131                    sb.append(item.toString());
1132                }
1133                sb.append(")");
1134                return sb.toString();
1135            }
1136    
1137            public void accept(MemberSetVisitor visitor) {
1138                visitor.visit(this);
1139            }
1140    
1141            public MemberSetPlus filter(RolapLevel level) {
1142                final List<MemberSetPlus> filteredItems =
1143                    new ArrayList<MemberSetPlus>();
1144                for (MemberSetPlus item : items) {
1145                    final MemberSetPlus filteredItem = item.filter(level);
1146                    if (filteredItem == EmptyMemberSet.INSTANCE) {
1147                        // skip it
1148                    } else {
1149                        assert !(filteredItem instanceof EmptyMemberSet);
1150                        filteredItems.add(filteredItem);
1151                    }
1152                }
1153                if (filteredItems.isEmpty()) {
1154                    return EmptyMemberSet.INSTANCE;
1155                } else if (filteredItems.equals(items)) {
1156                    return this;
1157                } else {
1158                    return new UnionMemberSet(filteredItems);
1159                }
1160            }
1161        }
1162    
1163        /**
1164         * Member set defined by a range of members between a lower and upper
1165         * bound.
1166         */
1167        static class RangeMemberSet implements MemberSetPlus {
1168            private final RolapMember lowerMember;
1169            private final boolean lowerInclusive;
1170            private final RolapMember upperMember;
1171            private final boolean upperInclusive;
1172            private final boolean descendants;
1173            private final RolapLevel level;
1174    
1175            RangeMemberSet(
1176                RolapMember lowerMember,
1177                boolean lowerInclusive,
1178                RolapMember upperMember,
1179                boolean upperInclusive,
1180                boolean descendants)
1181            {
1182                assert lowerMember != null || upperMember != null;
1183                assert lowerMember == null
1184                    || upperMember == null
1185                    || lowerMember.getLevel() == upperMember.getLevel();
1186                assert !(lowerMember == null && lowerInclusive);
1187                assert !(upperMember == null && upperInclusive);
1188                assert !(lowerMember instanceof RolapCubeMember);
1189                assert !(upperMember instanceof RolapCubeMember);
1190                this.lowerMember = lowerMember;
1191                this.lowerInclusive = lowerInclusive;
1192                this.upperMember = upperMember;
1193                this.upperInclusive = upperInclusive;
1194                this.descendants = descendants;
1195                this.level = lowerMember == null ?
1196                    upperMember.getLevel() :
1197                    lowerMember.getLevel();
1198            }
1199    
1200            public String toString() {
1201                final StringBuilder sb = new StringBuilder("Range(");
1202                if (lowerMember == null) {
1203                    sb.append("null");
1204                } else {
1205                    sb.append(lowerMember);
1206                    if (lowerInclusive) {
1207                        sb.append(" inclusive");
1208                    } else {
1209                        sb.append(" exclusive");
1210                    }
1211                }
1212                sb.append(" to ");
1213                if (upperMember == null) {
1214                    sb.append("null");
1215                } else {
1216                    sb.append(upperMember);
1217                    if (upperInclusive) {
1218                        sb.append(" inclusive");
1219                    } else {
1220                        sb.append(" exclusive");
1221                    }
1222                }
1223                sb.append(")");
1224                return sb.toString();
1225            }
1226    
1227            public void accept(MemberSetVisitor visitor) {
1228                // Don't traverse the range here: may not want to load it into cache
1229                visitor.visit(this);
1230            }
1231    
1232            public MemberSetPlus filter(RolapLevel level) {
1233                if (level == this.level) {
1234                    return this;
1235                } else {
1236                    return filter2(level, this.level, lowerMember, upperMember);
1237                }
1238            }
1239    
1240            public MemberSetPlus filter2(
1241                RolapLevel seekLevel,
1242                RolapLevel level,
1243                RolapMember lower,
1244                RolapMember upper)
1245            {
1246                if (level == seekLevel) {
1247                    return new RangeMemberSet(
1248                        lower, lowerInclusive, upper, upperInclusive, false);
1249                } else if (descendants
1250                    && level.getHierarchy() == seekLevel.getHierarchy()
1251                    && level.getDepth() < seekLevel.getDepth())
1252                {
1253                    final MemberReader memberReader =
1254                        level.getHierarchy().getMemberReader();
1255                    final List<RolapMember> list = new ArrayList<RolapMember>();
1256                    memberReader.getMemberChildren(lower, list);
1257                    if (list.isEmpty()) {
1258                        return EmptyMemberSet.INSTANCE;
1259                    }
1260                    RolapMember lowerChild = list.get(0);
1261                    list.clear();
1262                    memberReader.getMemberChildren(upper, list);
1263                    if (list.isEmpty()) {
1264                        return EmptyMemberSet.INSTANCE;
1265                    }
1266                    RolapMember upperChild = list.get(list.size() - 1);
1267                    return filter2(
1268                        seekLevel, (RolapLevel) level.getChildLevel(),
1269                        lowerChild, upperChild);
1270                } else {
1271                    return EmptyMemberSet.INSTANCE;
1272                }
1273            }
1274        }
1275    
1276        /**
1277         * Command consisting of a set of commands executed in sequence.
1278         */
1279        private static class CompoundCommand implements MemberEditCommandPlus {
1280            private final List<MemberEditCommandPlus> commandList;
1281    
1282            CompoundCommand(List<MemberEditCommandPlus> commandList) {
1283                this.commandList = commandList;
1284            }
1285    
1286            public String toString() {
1287                return Util.commaList("Compound", commandList);
1288            }
1289    
1290            public void execute(final List<CellRegion> cellRegionList) {
1291                for (MemberEditCommandPlus command : commandList) {
1292                    command.execute(cellRegionList);
1293                }
1294            }
1295        }
1296    
1297        /**
1298         * Command that deletes a member and its descendants from the cache.
1299         */
1300        private class DeleteMemberCommand
1301            extends MemberSetVisitorImpl
1302            implements MemberEditCommandPlus
1303        {
1304            private final MemberSetPlus set;
1305            private List<CellRegion> cellRegionList;
1306    
1307            DeleteMemberCommand(MemberSetPlus set) {
1308                this.set = set;
1309            }
1310    
1311            public String toString() {
1312                return "DeleteMemberCommand(" + set + ")";
1313            }
1314    
1315            public void execute(final List<CellRegion> cellRegionList) {
1316                // NOTE: use of member makes this class non-reentrant
1317                this.cellRegionList = cellRegionList;
1318                set.accept(this);
1319                this.cellRegionList = null;
1320            }
1321    
1322            public void visit(RolapMember member) {
1323                deleteMember(member, member.getParentMember(), cellRegionList);
1324            }
1325        }
1326    
1327        /**
1328         * Command that adds a new member to the cache.
1329         */
1330        private class AddMemberCommand implements MemberEditCommandPlus {
1331            private final RolapMember member;
1332    
1333            public AddMemberCommand(RolapMember member) {
1334                assert member != null;
1335                this.member = stripMember(member);
1336            }
1337    
1338            public String toString() {
1339                return "AddMemberCommand(" + member + ")";
1340            }
1341    
1342            public void execute(List<CellRegion> cellRegionList) {
1343                addMember(member, member.getParentMember(), cellRegionList);
1344            }
1345        }
1346    
1347        /**
1348         * Command that moves a member to a new parent.
1349         */
1350        private class MoveMemberCommand implements MemberEditCommandPlus {
1351            private final RolapMember member;
1352            private final RolapMember newParent;
1353    
1354            MoveMemberCommand(RolapMember member, RolapMember newParent) {
1355                this.member = member;
1356                this.newParent = newParent;
1357            }
1358    
1359            public String toString() {
1360                return "MoveMemberCommand(" + member + ", " + newParent + ")";
1361            }
1362    
1363            public void execute(final List<CellRegion> cellRegionList) {
1364                deleteMember(member, member.getParentMember(), cellRegionList);
1365                member.setParentMember(newParent);
1366                addMember(member, member.getParentMember(), cellRegionList);
1367            }
1368        }
1369    
1370        /**
1371         * Command that changes one or more properties of a member.
1372         */
1373        private class ChangeMemberPropsCommand
1374            extends MemberSetVisitorImpl
1375            implements MemberEditCommandPlus
1376        {
1377            final MemberSetPlus memberSet;
1378            final Map<String, Object> propertyValues;
1379    
1380            ChangeMemberPropsCommand(
1381                MemberSetPlus memberSet,
1382                Map<String, Object> propertyValues)
1383            {
1384                this.memberSet = memberSet;
1385                this.propertyValues = propertyValues;
1386            }
1387    
1388            public String toString() {
1389                return "CreateMemberPropsCommand(" + memberSet
1390                    + ", " + propertyValues + ")";
1391            }
1392    
1393            public void execute(List<CellRegion> cellRegionList) {
1394                // ignore cellRegionList - no changes to cell cache
1395                memberSet.accept(this);
1396            }
1397    
1398            public void visit(RolapMember member) {
1399                // Change member's properties.
1400                member = stripMember(member);
1401                final MemberCache memberCache = getMemberCache(member);
1402                final Object cacheKey =
1403                    memberCache.makeKey(
1404                        member.getParentMember(),
1405                        member.getKey());
1406                final RolapMember cacheMember = memberCache.getMember(cacheKey);
1407                if (cacheMember == null) {
1408                    return;
1409                }
1410                for (Map.Entry<String, Object> entry : propertyValues.entrySet()) {
1411                    cacheMember.setProperty(entry.getKey(), entry.getValue());
1412                }
1413            }
1414        }
1415    
1416        private static RolapMember stripMember(RolapMember member) {
1417            if (member instanceof RolapCubeMember) {
1418                member = ((RolapCubeMember) member).rolapMember;
1419            }
1420            return member;
1421        }
1422    
1423        private static void stripMemberList(List<RolapMember> members) {
1424            for (int i = 0; i < members.size(); i++) {
1425                RolapMember member = members.get(i);
1426                if (member instanceof RolapCubeMember) {
1427                    members.set(i, ((RolapCubeMember) member).rolapMember);
1428                }
1429            }
1430        }
1431    
1432        private void deleteMember(
1433            final RolapMember member,
1434            RolapMember previousParent,
1435            List<CellRegion> cellRegionList)
1436        {
1437            // Remove member from cache,
1438            // and that will remove the parent's children list.
1439            final MemberCache memberCache = getMemberCache(member);
1440            final Object key =
1441                memberCache.makeKey(previousParent, member.getKey());
1442            memberCache.removeMember(key);
1443    
1444            // Cells for member and its ancestors are now invalid. It's sufficient
1445            // to flush the member.
1446            cellRegionList.add(createMemberRegion(member, true));
1447        }
1448    
1449        /**
1450         * Adds a member to cache.
1451         *
1452         * @param member Member
1453         * @param parent Member's parent (generally equals member.getParentMember)
1454         * @param cellRegionList List of cell regions to be flushed
1455         */
1456        private void addMember(
1457            final RolapMember member,
1458            final RolapMember parent,
1459            List<CellRegion> cellRegionList)
1460        {
1461            // Parent's children are now invalid. Remove parent from cache,
1462            // and that will remove the parent's children list. Children lists
1463            // of its existing children can remain in cache.
1464            final MemberCache memberCache = getMemberCache(member);
1465            final Object parentKey =
1466                memberCache.makeKey(parent.getParentMember(), parent.getKey());
1467            memberCache.removeMember(parentKey);
1468    
1469            // Cells for all of member's ancestors are now invalid. It's sufficient
1470            // to flush its parent.
1471            cellRegionList.add(createMemberRegion(parent, false));
1472        }
1473    
1474        /**
1475         * Removes a member from cache.
1476         *
1477         * @param member Member
1478         * @param cellRegionList Populated with cell region to be flushed
1479         */
1480        private void flushMember(
1481            RolapMember member,
1482            List<CellRegion> cellRegionList)
1483        {
1484            // TODO
1485    
1486            cellRegionList.add(createMemberRegion(member, false));
1487        }
1488    }
1489    
1490    // End CacheControlImpl.java