001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2005-2005 Julian Hyde
008// Copyright (C) 2005-2012 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap.aggmatcher;
012
013import mondrian.olap.*;
014import mondrian.recorder.MessageRecorder;
015import mondrian.resource.MondrianResource;
016import mondrian.rolap.*;
017
018import org.apache.log4j.Logger;
019
020import java.io.PrintWriter;
021import java.io.StringWriter;
022import java.util.*;
023import java.util.regex.Pattern;
024
025/**
026 * A class containing a RolapCube's Aggregate tables exclude/include
027 * criteria.
028 *
029 * @author Richard M. Emberson
030 */
031public class ExplicitRules {
032    private static final Logger LOGGER = Logger.getLogger(ExplicitRules.class);
033
034    private static final MondrianResource mres = MondrianResource.instance();
035
036    /**
037     * Returns whether the given is tableName explicitly excluded from
038     * consideration as a candidate aggregate table.
039     */
040    public static boolean excludeTable(
041        final String tableName,
042        final List<Group> aggGroups)
043    {
044        for (Group group : aggGroups) {
045            if (group.excludeTable(tableName)) {
046                return true;
047            }
048        }
049        return false;
050    }
051
052    /**
053     * Returns the {@link TableDef} for a tableName that is a candidate
054     * aggregate table. If null is returned, then the default rules are used
055     * otherwise if not null, then the ExplicitRules.TableDef is used.
056     */
057    public static ExplicitRules.TableDef getIncludeByTableDef(
058        final String tableName,
059        final List<Group> aggGroups)
060    {
061        for (Group group : aggGroups) {
062            TableDef tableDef = group.getIncludeByTableDef(tableName);
063            if (tableDef != null) {
064                return tableDef;
065            }
066        }
067        return null;
068    }
069
070    /**
071     * This class forms a collection of aggregate table explicit rules for a
072     * given cube.
073     *
074     */
075    public static class Group {
076
077        /**
078         * Make an ExplicitRules.Group for a given RolapCube given the
079         * MondrianDef.Cube associated with that cube.
080         */
081        public static ExplicitRules.Group make(
082            final RolapCube cube,
083            final MondrianDef.Cube xmlCube)
084        {
085            Group group = new Group(cube);
086
087            MondrianDef.Relation relation = xmlCube.fact;
088
089            if (relation instanceof MondrianDef.Table) {
090                MondrianDef.AggExclude[] aggExcludes =
091                    ((MondrianDef.Table) relation).getAggExcludes();
092                if (aggExcludes != null) {
093                    for (MondrianDef.AggExclude aggExclude : aggExcludes) {
094                        Exclude exclude =
095                            ExplicitRules.make(aggExclude);
096                        group.addExclude(exclude);
097                    }
098                }
099                MondrianDef.AggTable[] aggTables =
100                    ((MondrianDef.Table) relation).getAggTables();
101                if (aggTables != null) {
102                    for (MondrianDef.AggTable aggTable : aggTables) {
103                        TableDef tableDef = TableDef.make(aggTable, group);
104                        group.addTableDef(tableDef);
105                    }
106                }
107            } else {
108                LOGGER.warn(
109                    mres.CubeRelationNotTable.str(
110                        cube.getName(),
111                        relation.getClass().getName()));
112            }
113
114            if (LOGGER.isDebugEnabled()) {
115                LOGGER.debug(Util.nl + group);
116            }
117            return group;
118        }
119
120        private final RolapCube cube;
121        private List<TableDef> tableDefs;
122        private List<Exclude> excludes;
123
124        public Group(final RolapCube cube) {
125            this.cube = cube;
126            this.excludes = Collections.emptyList();
127            this.tableDefs = Collections.emptyList();
128        }
129
130        /**
131         * Get the RolapCube associated with this Group.
132         */
133        public RolapCube getCube() {
134            return cube;
135        }
136
137        /**
138         * Get the RolapStar associated with this Group's RolapCube.
139         */
140        public RolapStar getStar() {
141            return getCube().getStar();
142        }
143
144        /**
145         * Get the name of this Group (its the name of its RolapCube).
146         */
147        public String getName() {
148            return getCube().getName();
149        }
150
151        /**
152         * Are there any rules associated with this Group.
153         */
154        public boolean hasRules() {
155            return
156                (excludes != Collections.EMPTY_LIST)
157                || (tableDefs != Collections.EMPTY_LIST);
158        }
159
160        /**
161         * Add an exclude rule.
162         */
163        public void addExclude(final ExplicitRules.Exclude exclude) {
164            if (excludes == Collections.EMPTY_LIST) {
165                excludes = new ArrayList<Exclude>();
166            }
167            excludes.add(exclude);
168        }
169
170        /**
171         * Add a name or pattern (table) rule.
172         */
173        public void addTableDef(final ExplicitRules.TableDef tableDef) {
174            if (tableDefs == Collections.EMPTY_LIST) {
175                tableDefs = new ArrayList<TableDef>();
176            }
177            tableDefs.add(tableDef);
178        }
179
180        /**
181         * Returns whether the given tableName is excluded.
182         */
183        public boolean excludeTable(final String tableName) {
184            // See if the table is explicitly, by name, excluded
185            for (Exclude exclude : excludes) {
186                if (exclude.isExcluded(tableName)) {
187                    return true;
188                }
189            }
190            return false;
191        }
192
193        /**
194         * Is the given tableName included either by exact name or by pattern.
195         */
196        public ExplicitRules.TableDef getIncludeByTableDef(
197            final String tableName)
198        {
199            // An exact match on a NameTableDef takes precedences over a
200            // fuzzy match on a PatternTableDef, so
201            // first look throught NameTableDef then PatternTableDef
202            for (ExplicitRules.TableDef tableDef : tableDefs) {
203                if (tableDef instanceof NameTableDef) {
204                    if (tableDef.matches(tableName)) {
205                        return tableDef;
206                    }
207                }
208            }
209            for (ExplicitRules.TableDef tableDef : tableDefs) {
210                if (tableDef instanceof PatternTableDef) {
211                    if (tableDef.matches(tableName)) {
212                        return tableDef;
213                    }
214                }
215            }
216            return null;
217        }
218
219        /**
220         * Get the database table name associated with this Group's RolapStar's
221         * fact table.
222         */
223        public String getTableName() {
224            RolapStar.Table table = getStar().getFactTable();
225            MondrianDef.Relation relation = table.getRelation();
226            return relation.getAlias();
227        }
228
229        /**
230         * Get the database schema name associated with this Group's RolapStar's
231         * fact table.
232         */
233        public String getSchemaName() {
234            String schema = null;
235
236            RolapStar.Table table = getStar().getFactTable();
237            MondrianDef.Relation relation = table.getRelation();
238
239            if (relation instanceof MondrianDef.Table) {
240                MondrianDef.Table mtable = (MondrianDef.Table) relation;
241                schema = mtable.schema;
242            }
243            return schema;
244        }
245        /**
246         * Get the database catalog name associated with this Group's
247         * RolapStar's fact table.
248         * Note: this currently this always returns null.
249         */
250        public String getCatalogName() {
251            return null;
252        }
253
254        /**
255         * Validate the content and structure of this Group.
256         */
257        public void validate(final MessageRecorder msgRecorder) {
258            msgRecorder.pushContextName(getName());
259            try {
260                for (ExplicitRules.TableDef tableDef : tableDefs) {
261                    tableDef.validate(msgRecorder);
262                }
263            } finally {
264                msgRecorder.popContextName();
265            }
266        }
267
268        public String toString() {
269            StringWriter sw = new StringWriter(256);
270            PrintWriter pw = new PrintWriter(sw);
271            print(pw, "");
272            pw.flush();
273            return sw.toString();
274        }
275
276        public void print(final PrintWriter pw, final String prefix) {
277            pw.print(prefix);
278            pw.println("ExplicitRules.Group:");
279            String subprefix = prefix + "  ";
280            String subsubprefix = subprefix + "  ";
281
282            pw.print(subprefix);
283            pw.print("name=");
284            pw.println(getStar().getFactTable().getRelation());
285
286            pw.print(subprefix);
287            pw.println("TableDefs: [");
288            for (ExplicitRules.TableDef tableDef : tableDefs) {
289                tableDef.print(pw, subsubprefix);
290            }
291            pw.print(subprefix);
292            pw.println("]");
293        }
294    }
295
296    private static Exclude make(final MondrianDef.AggExclude aggExclude) {
297        return (aggExclude.getNameAttribute() != null)
298            ? new ExcludeName(
299                aggExclude.getNameAttribute(),
300                aggExclude.isIgnoreCase())
301            : (Exclude) new ExcludePattern(
302                aggExclude.getPattern(),
303                aggExclude.isIgnoreCase());
304    }
305
306    /**
307     * Interface of an Exclude type. There are two implementations, one that
308     * excludes by exact name match (as an option, ignore case) and the second
309     * that matches a regular expression.
310     */
311    private interface Exclude {
312        /**
313         * Return true if the tableName is excluded.
314         *
315         * @param tableName Table name
316         * @return whether table name is excluded
317         */
318        boolean isExcluded(final String tableName);
319
320        /**
321         * Validate that the exclude name matches the table pattern.
322         *
323         * @param msgRecorder Message recorder
324         */
325        void validate(final MessageRecorder msgRecorder);
326
327        /**
328         * Prints this rule to a PrintWriter.
329         * @param prefix Line prefix, for indentation
330         */
331        void print(final PrintWriter pw, final String prefix);
332    }
333
334    /**
335     * Implementation of Exclude which matches names exactly.
336     */
337    private static class ExcludeName implements Exclude {
338        private final String name;
339        private final boolean ignoreCase;
340
341        private ExcludeName(final String name, final boolean ignoreCase) {
342            this.name = name;
343            this.ignoreCase = ignoreCase;
344        }
345
346        /**
347         * Returns the name to be matched.
348         */
349        public String getName() {
350            return name;
351        }
352
353        /**
354         * Returns true if the matching can ignore case.
355         */
356        public boolean isIgnoreCase() {
357            return ignoreCase;
358        }
359
360        public boolean isExcluded(final String tableName) {
361            return (this.ignoreCase)
362                ? this.name.equals(tableName)
363                : this.name.equalsIgnoreCase(tableName);
364        }
365
366        public void validate(final MessageRecorder msgRecorder) {
367            msgRecorder.pushContextName("ExcludeName");
368            try {
369                String name = getName();
370                checkAttributeString(msgRecorder, name, "name");
371
372
373// RME TODO
374//                // If name does not match the PatternTableDef pattern,
375//                // then issue warning.
376//                // Why, because no table with the exclude's name will
377//                // ever match the pattern, so the exclude is superfluous.
378//                // This is best effort.
379//                Pattern pattern =
380//                    ExplicitRules.PatternTableDef.this.getPattern();
381//                boolean patternIgnoreCase =
382//                    ExplicitRules.PatternTableDef.this.isIgnoreCase();
383//                boolean ignoreCase = isIgnoreCase();
384//
385//                // If pattern is ignoreCase and name is any case or pattern
386//                // is not ignoreCase and name is not ignoreCase, then simply
387//                // see if name matches.
388//                // Else pattern in not ignoreCase and name is ignoreCase,
389//                // then pattern could be "AB.*" and name "abc".
390//                // Here "abc" would name, but not pattern - but who cares
391//                if (patternIgnoreCase || ! ignoreCase) {
392//                    if (! pattern.matcher(name).matches()) {
393//                        msgRecorder.reportWarning(
394//                            mres.getSuperfluousExludeName(
395//                                        msgRecorder.getContext(),
396//                                        name,
397//                                        pattern.pattern()));
398//                    }
399//                }
400            } finally {
401                msgRecorder.popContextName();
402            }
403        }
404
405        public void print(final PrintWriter pw, final String prefix) {
406            pw.print(prefix);
407            pw.println("ExplicitRules.PatternTableDef.ExcludeName:");
408
409            String subprefix = prefix + "  ";
410
411            pw.print(subprefix);
412            pw.print("name=");
413            pw.println(this.name);
414
415            pw.print(subprefix);
416            pw.print("ignoreCase=");
417            pw.println(this.ignoreCase);
418        }
419    }
420
421    /**
422     * This class is a regular expression base name matching Exclude
423     * implementation.
424     */
425    private static class ExcludePattern implements Exclude {
426        private final Pattern pattern;
427
428        private ExcludePattern(
429            final String pattern,
430            final boolean ignoreCase)
431        {
432            this.pattern = (ignoreCase)
433                ? Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
434                : Pattern.compile(pattern);
435        }
436
437        public boolean isExcluded(final String tableName) {
438            return pattern.matcher(tableName).matches();
439        }
440
441        public void validate(final MessageRecorder msgRecorder) {
442            msgRecorder.pushContextName("ExcludePattern");
443            try {
444                checkAttributeString(
445                    msgRecorder,
446                    pattern.pattern(),
447                    "pattern");
448                //String context = msgRecorder.getContext();
449                // Is there any way to determine if the exclude pattern
450                // is never a sub-set of the table pattern.
451                // I will have to think about this.
452                // Until then, this method is empty.
453            } finally {
454                msgRecorder.popContextName();
455            }
456        }
457
458        public void print(final PrintWriter pw, final String prefix) {
459            pw.print(prefix);
460            pw.println("ExplicitRules.PatternTableDef.ExcludePattern:");
461
462            String subprefix = prefix + "  ";
463
464            pw.print(subprefix);
465            pw.print("pattern=");
466            pw.print(this.pattern.pattern());
467            pw.print(":");
468            pw.println(this.pattern.flags());
469        }
470    }
471
472    /**
473     * This is the base class for the exact name based and name pattern based
474     * aggregate table mapping definitions. It contains the mappings for the
475     * fact count column, optional ignore columns, foreign key mappings,
476     * measure column mappings and level column mappings.
477     */
478    public static abstract class TableDef {
479
480        /**
481         * Given a MondrianDef.AggTable instance create a TableDef instance
482         * which is either a NameTableDef or PatternTableDef.
483         */
484        static ExplicitRules.TableDef make(
485            final MondrianDef.AggTable aggTable,
486            final ExplicitRules.Group group)
487        {
488            return (aggTable instanceof MondrianDef.AggName)
489                ? ExplicitRules.NameTableDef.make(
490                    (MondrianDef.AggName) aggTable, group)
491                : (ExplicitRules.TableDef)
492                ExplicitRules.PatternTableDef.make(
493                    (MondrianDef.AggPattern) aggTable, group);
494        }
495
496        /**
497         * This method extracts information from the MondrianDef.AggTable and
498         * places it in the ExplicitRules.TableDef. This code is used for both
499         * the NameTableDef and PatternTableDef subclasses of TableDef (it
500         * extracts information common to both).
501         */
502        private static void add(
503            final ExplicitRules.TableDef tableDef,
504            final MondrianDef.AggTable aggTable)
505        {
506            if (aggTable.getAggFactCount() != null) {
507                tableDef.setFactCountName(
508                    aggTable.getAggFactCount().getColumnName());
509            }
510
511            MondrianDef.AggIgnoreColumn[] ignores =
512                aggTable.getAggIgnoreColumns();
513
514            if (ignores != null) {
515                for (MondrianDef.AggIgnoreColumn ignore : ignores) {
516                    tableDef.addIgnoreColumnName(ignore.getColumnName());
517                }
518            }
519
520            MondrianDef.AggForeignKey[] fks = aggTable.getAggForeignKeys();
521            if (fks != null) {
522                for (MondrianDef.AggForeignKey fk : fks) {
523                    tableDef.addFK(fk);
524                }
525            }
526            MondrianDef.AggMeasure[] measures = aggTable.getAggMeasures();
527            if (measures != null) {
528                for (MondrianDef.AggMeasure measure : measures) {
529                    addTo(tableDef, measure);
530                }
531            }
532
533            MondrianDef.AggLevel[] levels = aggTable.getAggLevels();
534            if (levels != null) {
535                for (MondrianDef.AggLevel level : levels) {
536                    addTo(tableDef, level);
537                }
538            }
539        }
540
541        private static void addTo(
542            final ExplicitRules.TableDef tableDef,
543            final MondrianDef.AggLevel aggLevel)
544        {
545            addLevelTo(
546                tableDef,
547                aggLevel.getNameAttribute(),
548                aggLevel.getColumnName(),
549                aggLevel.isCollapsed());
550        }
551
552        private static void addTo(
553            final ExplicitRules.TableDef tableDef,
554            final MondrianDef.AggMeasure aggMeasure)
555        {
556            addMeasureTo(
557                tableDef,
558                aggMeasure.getNameAttribute(),
559                aggMeasure.getColumn());
560        }
561
562        public static void addLevelTo(
563            final ExplicitRules.TableDef tableDef,
564            final String name,
565            final String columnName,
566            final boolean collapsed)
567        {
568            Level level = tableDef.new Level(name, columnName, collapsed);
569            tableDef.add(level);
570        }
571
572        public static void addMeasureTo(
573            final ExplicitRules.TableDef tableDef,
574            final String name,
575            final String column)
576        {
577            Measure measure = tableDef.new Measure(name, column);
578            tableDef.add(measure);
579        }
580
581        /**
582         * This class is used to map from a Level's symbolic name,
583         * [Time]&#46;[Year] to the aggregate table's column name, TIME_YEAR.
584         */
585        class Level {
586            private final String name;
587            private final String columnName;
588            private final boolean collapsed;
589            private RolapLevel rlevel;
590
591            Level(
592                final String name,
593                final String columnName,
594                final boolean collapsed)
595            {
596                this.name = name;
597                this.columnName = columnName;
598                this.collapsed = collapsed;
599            }
600
601            /**
602             * Get the symbolic name, the level name.
603             */
604            public String getName() {
605                return name;
606            }
607
608            /**
609             * Get the foreign key column name of the aggregate table.
610             */
611            public String getColumnName() {
612                return columnName;
613            }
614
615            /**
616             * Returns whether this level is collapsed (includes
617             * parent levels in the agg table).
618             */
619            public boolean isCollapsed() {
620                return collapsed;
621            }
622
623            /**
624             * Get the RolapLevel associated with level name.
625             */
626            public RolapLevel getRolapLevel() {
627                return rlevel;
628            }
629
630            /**
631             * Validates a level's name.
632             *
633             * <p>The level name must be of the form <code>[hierarchy usage
634             * name].[level name]</code>.
635             *
636             * <p>This method checks that is of length 2, starts with a
637             * hierarchy and the "level name" exists.
638             */
639            public void validate(final MessageRecorder msgRecorder) {
640                msgRecorder.pushContextName("Level");
641                try {
642                    String name = getName();
643                    String columnName = getColumnName();
644                    checkAttributeString(msgRecorder, name, "name");
645                    checkAttributeString(msgRecorder, columnName, "column");
646
647                    List<Id.Segment> names = Util.parseIdentifier(name);
648                    // must be [hierarchy usage name].[level name]
649                    if (!(names.size() == 2
650                        || MondrianProperties.instance().SsasCompatibleNaming
651                        .get()
652                        && names.size() == 3))
653                    {
654                        msgRecorder.reportError(
655                            mres.BadLevelNameFormat.str(
656                                msgRecorder.getContext(),
657                                name));
658                    } else {
659                        RolapCube cube = ExplicitRules.TableDef.this.getCube();
660                        SchemaReader schemaReader = cube.getSchemaReader();
661                        RolapLevel level =
662                            (RolapLevel) schemaReader.lookupCompound(
663                                cube,
664                                names,
665                                false,
666                                Category.Level);
667                        if (level == null) {
668                            Hierarchy hierarchy = (Hierarchy)
669                                schemaReader.lookupCompound(
670                                    cube,
671                                    names.subList(0, 1),
672                                    false,
673                                    Category.Hierarchy);
674                            if (hierarchy == null) {
675                                msgRecorder.reportError(
676                                    mres.UnknownHierarchyName.str(
677                                        msgRecorder.getContext(),
678                                        names.get(0).toString()));
679                            } else {
680                                msgRecorder.reportError(
681                                    mres.UnknownLevelName.str(
682                                        msgRecorder.getContext(),
683                                        names.get(0).toString(),
684                                        names.get(1).toString()));
685                            }
686                        }
687                        rlevel = level;
688                    }
689                } finally {
690                    msgRecorder.popContextName();
691                }
692            }
693
694            public String toString() {
695                StringWriter sw = new StringWriter(256);
696                PrintWriter pw = new PrintWriter(sw);
697                print(pw, "");
698                pw.flush();
699                return sw.toString();
700            }
701
702            public void print(final PrintWriter pw, final String prefix) {
703                pw.print(prefix);
704                pw.println("Level:");
705                String subprefix = prefix + "  ";
706
707                pw.print(subprefix);
708                pw.print("name=");
709                pw.println(this.name);
710
711                pw.print(subprefix);
712                pw.print("columnName=");
713                pw.println(this.columnName);
714            }
715        }
716
717        /**
718         * This class is used to map from a measure's symbolic name,
719         * [Measures]&amp;#46;[Unit Sales] to the aggregate table's column
720         * name, UNIT_SALES_SUM.
721         */
722        class Measure {
723            private final String name;
724            private String symbolicName;
725            private final String columnName;
726            private RolapStar.Measure rolapMeasure;
727
728            Measure(final String name, final String columnName) {
729                this.name = name;
730                this.columnName = columnName;
731            }
732
733            /**
734             * Get the symbolic name, the measure name, i.e.,
735             * [Measures].[Unit Sales].
736             */
737            public String getName() {
738                return name;
739            }
740
741            /**
742             * Get the symbolic name, the measure name, i.e., [Unit Sales].
743             */
744            public String getSymbolicName() {
745                return symbolicName;
746            }
747
748            /**
749             * Get the aggregate table column name.
750             */
751            public String getColumnName() {
752                return columnName;
753            }
754
755            /**
756             * Get the RolapStar.Measure associated with this symbolic name.
757             */
758            public RolapStar.Measure getRolapStarMeasure() {
759                return rolapMeasure;
760            }
761
762            /**
763             * Validates a measure's name.
764             *
765             * <p>The measure name must be of the form
766             * <blockquote><code>[Measures].[measure name]</code></blockquote>
767             *
768             * <p>This method checks that is of length 2, starts
769             * with "Measures" and the "measure name" exists.
770             */
771            public void validate(final MessageRecorder msgRecorder) {
772                msgRecorder.pushContextName("Measure");
773                try {
774                    String name = getName();
775                    String column = getColumnName();
776                    checkAttributeString(msgRecorder, name, "name");
777                    checkAttributeString(msgRecorder, column, "column");
778
779                    List<Id.Segment> names = Util.parseIdentifier(name);
780                    if (names.size() != 2) {
781                        msgRecorder.reportError(
782                            mres.BadMeasureNameFormat.str(
783                                msgRecorder.getContext(),
784                                name));
785                    } else {
786                        RolapCube cube = ExplicitRules.TableDef.this.getCube();
787                        SchemaReader schemaReader = cube.getSchemaReader();
788                        Member member = (Member) schemaReader.lookupCompound(
789                            cube,
790                            names,
791                            false,
792                            Category.Member);
793                        if (member == null) {
794                            if (!(names.get(0) instanceof Id.NameSegment
795                                    && ((Id.NameSegment) names.get(0)).name
796                                        .equals("Measures")))
797                            {
798                                msgRecorder.reportError(
799                                    mres.BadMeasures.str(
800                                        msgRecorder.getContext(),
801                                        names.get(0).toString()));
802                            } else {
803                                msgRecorder.reportError(
804                                    mres.UnknownMeasureName.str(
805                                        msgRecorder.getContext(),
806                                        names.get(1).toString()));
807                            }
808                        }
809                        RolapStar star = cube.getStar();
810                        rolapMeasure =
811                            names.get(1) instanceof Id.NameSegment
812                                ? star.getFactTable().lookupMeasureByName(
813                                    cube.getName(),
814                                    ((Id.NameSegment) names.get(1)).name)
815                                : null;
816                        if (rolapMeasure == null) {
817                            msgRecorder.reportError(
818                                mres.BadMeasureName.str(
819                                    msgRecorder.getContext(),
820                                    names.get(1).toString(),
821                                    cube.getName()));
822                        }
823                        symbolicName = names.get(1).toString();
824                    }
825                } finally {
826                    msgRecorder.popContextName();
827                }
828            }
829
830            public String toString() {
831                StringWriter sw = new StringWriter(256);
832                PrintWriter pw = new PrintWriter(sw);
833                print(pw, "");
834                pw.flush();
835                return sw.toString();
836            }
837
838            public void print(final PrintWriter pw, final String prefix) {
839                pw.print(prefix);
840                pw.println("Measure:");
841                String subprefix = prefix + "  ";
842
843                pw.print(subprefix);
844                pw.print("name=");
845                pw.println(this.name);
846
847                pw.print(subprefix);
848                pw.print("column=");
849                pw.println(this.columnName);
850            }
851        }
852
853        private static int idCount = 0;
854        private static int nextId() {
855            return idCount++;
856        }
857
858        protected final int id;
859        protected final boolean ignoreCase;
860        protected final ExplicitRules.Group aggGroup;
861        protected String factCountName;
862        protected List<String> ignoreColumnNames;
863        private Map<String, String> foreignKeyMap;
864        private List<Level> levels;
865        private List<Measure> measures;
866        protected int approxRowCount = Integer.MIN_VALUE;
867
868        protected TableDef(
869            final boolean ignoreCase,
870            final ExplicitRules.Group aggGroup)
871        {
872            this.id = nextId();
873            this.ignoreCase = ignoreCase;
874            this.aggGroup = aggGroup;
875            this.foreignKeyMap = Collections.emptyMap();
876            this.levels = Collections.emptyList();
877            this.measures = Collections.emptyList();
878            this.ignoreColumnNames = Collections.emptyList();
879        }
880
881        /**
882         * Returns an approximate number of rows in this table.
883         * A negative value indicates that no estimate is available.
884         * @return An estimated row count, or a negative value if no
885         * row count approximation was available.
886         */
887        public int getApproxRowCount() {
888            return approxRowCount;
889        }
890
891        /**
892         * Return true if this name/pattern matching ignores case.
893         */
894        public boolean isIgnoreCase() {
895            return this.ignoreCase;
896        }
897
898        /**
899         * Get the RolapStar associated with this cube.
900         */
901        public RolapStar getStar() {
902            return getAggGroup().getStar();
903        }
904
905        /**
906         * Get the Group with which is a part.
907         */
908        public ExplicitRules.Group getAggGroup() {
909            return this.aggGroup;
910        }
911
912        /**
913         * Get the name of the fact count column.
914         */
915        protected String getFactCountName() {
916            return factCountName;
917        }
918
919        /**
920         * Set the name of the fact count column.
921         */
922        protected void setFactCountName(final String factCountName) {
923            this.factCountName = factCountName;
924        }
925
926        /**
927         * Get an Iterator over all ignore column name entries.
928         */
929        protected Iterator<String> getIgnoreColumnNames() {
930            return ignoreColumnNames.iterator();
931        }
932
933        /**
934         * Gets all level mappings.
935         */
936        public List<Level> getLevels() {
937            return levels;
938        }
939
940        /**
941         * Gets all level mappings.
942         */
943        public List<Measure> getMeasures() {
944            return measures;
945        }
946
947        /**
948         * Get Matcher for ignore columns.
949         */
950        protected Recognizer.Matcher getIgnoreMatcher() {
951            return new Recognizer.Matcher() {
952                public boolean matches(final String name) {
953                    for (Iterator<String> it =
954                            ExplicitRules.TableDef.this.getIgnoreColumnNames();
955                        it.hasNext();)
956                    {
957                        String ignoreName = it.next();
958                        if (isIgnoreCase()) {
959                            if (ignoreName.equalsIgnoreCase(name)) {
960                                return true;
961                            }
962                        } else {
963                            if (ignoreName.equals(name)) {
964                                return true;
965                            }
966                        }
967                    }
968                    return false;
969                }
970            };
971        }
972
973        /**
974         * Get Matcher for the fact count column.
975         */
976        protected Recognizer.Matcher getFactCountMatcher() {
977            return new Recognizer.Matcher() {
978                public boolean matches(String name) {
979                    // Match is case insensitive
980                    final String factCountName = TableDef.this.factCountName;
981                    return factCountName != null
982                        && factCountName.equalsIgnoreCase(name);
983                }
984            };
985        }
986
987        /**
988         * Get the RolapCube associated with this mapping.
989         */
990        RolapCube getCube() {
991            return aggGroup.getCube();
992        }
993
994        /**
995         * Checks that ALL of the columns in the dbTable have a mapping in the
996         * tableDef.
997         *
998         * <p>It is an error if there is a column that does not have a mapping.
999         */
1000        public boolean columnsOK(
1001            final RolapStar star,
1002            final JdbcSchema.Table dbFactTable,
1003            final JdbcSchema.Table dbTable,
1004            final MessageRecorder msgRecorder)
1005        {
1006            Recognizer cb =
1007                new ExplicitRecognizer(
1008                    this, star, getCube(), dbFactTable, dbTable, msgRecorder);
1009            return cb.check();
1010        }
1011
1012        /**
1013         * Adds the name of an aggregate table column that is to be ignored.
1014         */
1015        protected void addIgnoreColumnName(final String ignoreName) {
1016            if (this.ignoreColumnNames == Collections.EMPTY_LIST) {
1017                this.ignoreColumnNames = new ArrayList<String>();
1018            }
1019            this.ignoreColumnNames.add(ignoreName);
1020        }
1021
1022        /**
1023         * Add foreign key mapping entry (maps from fact table foreign key
1024         * column name to aggregate table foreign key column name).
1025         */
1026        protected void addFK(final MondrianDef.AggForeignKey fk) {
1027            if (this.foreignKeyMap == Collections.EMPTY_MAP) {
1028                this.foreignKeyMap = new HashMap<String, String>();
1029            }
1030            this.foreignKeyMap.put(
1031                fk.getFactFKColumnName(),
1032                fk.getAggregateFKColumnName());
1033        }
1034
1035        /**
1036         * Get the name of the aggregate table's foreign key column that matches
1037         * the base fact table's foreign key column or return null.
1038         */
1039        protected String getAggregateFK(final String baseFK) {
1040            return this.foreignKeyMap.get(baseFK);
1041        }
1042
1043        /**
1044         * Adds a Level.
1045         */
1046        protected void add(final Level level) {
1047            if (this.levels == Collections.EMPTY_LIST) {
1048                this.levels = new ArrayList<Level>();
1049            }
1050            this.levels.add(level);
1051        }
1052
1053        /**
1054         * Adds a Measure.
1055         */
1056        protected void add(final Measure measure) {
1057            if (this.measures == Collections.EMPTY_LIST) {
1058                this.measures = new ArrayList<Measure>();
1059            }
1060            this.measures.add(measure);
1061        }
1062
1063        /**
1064         * Does the TableDef match a table with name tableName.
1065         */
1066        public abstract boolean matches(final String tableName);
1067
1068        /**
1069         * Validate the Levels and Measures, also make sure each definition
1070         * is different, both name and column.
1071         */
1072        public void validate(final MessageRecorder msgRecorder) {
1073            msgRecorder.pushContextName("TableDef");
1074            try {
1075                // used to detect duplicates
1076                Map<String, Object> namesToObjects =
1077                    new HashMap<String, Object>();
1078                // used to detect duplicates
1079                Map<String, Object> columnsToObjects =
1080                    new HashMap<String, Object>();
1081
1082                for (Level level : levels) {
1083                    level.validate(msgRecorder);
1084
1085                    // Is the level name a duplicate
1086                    if (namesToObjects.containsKey(level.getName())) {
1087                        msgRecorder.reportError(
1088                            mres.DuplicateLevelNames.str(
1089                                msgRecorder.getContext(),
1090                                level.getName()));
1091                    } else {
1092                        namesToObjects.put(level.getName(), level);
1093                    }
1094
1095                    // Is the level foreign key name a duplicate
1096                    if (columnsToObjects.containsKey(level.getColumnName())) {
1097                        Level l = (Level)
1098                            columnsToObjects.get(level.getColumnName());
1099                        msgRecorder.reportError(
1100                            mres.DuplicateLevelColumnNames.str(
1101                                msgRecorder.getContext(),
1102                                level.getName(),
1103                                l.getName(),
1104                                level.getColumnName()));
1105                    } else {
1106                        columnsToObjects.put(level.getColumnName(), level);
1107                    }
1108                }
1109
1110                // reset names map, but keep the columns from levels
1111                namesToObjects.clear();
1112                for (Measure measure : measures) {
1113                    measure.validate(msgRecorder);
1114
1115                    if (namesToObjects.containsKey(measure.getName())) {
1116                        msgRecorder.reportError(
1117                            mres.DuplicateMeasureNames.str(
1118                                msgRecorder.getContext(),
1119                                measure.getName()));
1120                        continue;
1121                    } else {
1122                        namesToObjects.put(measure.getName(), measure);
1123                    }
1124
1125                    if (columnsToObjects.containsKey(measure.getColumnName())) {
1126                        Object o =
1127                            columnsToObjects.get(measure.getColumnName());
1128                        if (o instanceof Measure) {
1129                            Measure m = (Measure) o;
1130                            msgRecorder.reportError(
1131                                mres.DuplicateMeasureColumnNames.str(
1132                                    msgRecorder.getContext(),
1133                                    measure.getName(),
1134                                    m.getName(),
1135                                    measure.getColumnName()));
1136                        } else {
1137                            Level l = (Level) o;
1138                            msgRecorder.reportError(
1139                                mres.DuplicateLevelMeasureColumnNames.str(
1140                                    msgRecorder.getContext(),
1141                                    l.getName(),
1142                                    measure.getName(),
1143                                    measure.getColumnName()));
1144                        }
1145
1146                    } else {
1147                        columnsToObjects.put(measure.getColumnName(), measure);
1148                    }
1149                }
1150
1151                // reset both
1152                namesToObjects.clear();
1153                columnsToObjects.clear();
1154
1155                // Make sure that the base fact table foreign key names match
1156                // real columns
1157                RolapStar star = getStar();
1158                RolapStar.Table factTable = star.getFactTable();
1159                String tableName = factTable.getAlias();
1160                for (Map.Entry<String, String> e : foreignKeyMap.entrySet()) {
1161                    String baseFKName = e.getKey();
1162                    String aggFKName = e.getValue();
1163
1164                    if (namesToObjects.containsKey(baseFKName)) {
1165                        msgRecorder.reportError(
1166                            mres.DuplicateFactForeignKey.str(
1167                                msgRecorder.getContext(),
1168                                baseFKName,
1169                                aggFKName));
1170                    } else {
1171                        namesToObjects.put(baseFKName, aggFKName);
1172                    }
1173                    if (columnsToObjects.containsKey(aggFKName)) {
1174                        msgRecorder.reportError(
1175                            mres.DuplicateFactForeignKey.str(
1176                                msgRecorder.getContext(),
1177                                baseFKName,
1178                                aggFKName));
1179                    } else {
1180                        columnsToObjects.put(aggFKName, baseFKName);
1181                    }
1182
1183                    MondrianDef.Column c =
1184                        new MondrianDef.Column(tableName, baseFKName);
1185                    if (factTable.findTableWithLeftCondition(c) == null) {
1186                        msgRecorder.reportError(
1187                            mres.UnknownLeftJoinCondition.str(
1188                                msgRecorder.getContext(),
1189                                tableName,
1190                                baseFKName));
1191                    }
1192                }
1193            } finally {
1194                msgRecorder.popContextName();
1195            }
1196        }
1197
1198        public String toString() {
1199            StringWriter sw = new StringWriter(256);
1200            PrintWriter pw = new PrintWriter(sw);
1201            print(pw, "");
1202            pw.flush();
1203            return sw.toString();
1204        }
1205
1206        public void print(final PrintWriter pw, final String prefix) {
1207            String subprefix = prefix + "  ";
1208            String subsubprefix = subprefix + "  ";
1209
1210            pw.print(subprefix);
1211            pw.print("id=");
1212            pw.println(this.id);
1213
1214            pw.print(subprefix);
1215            pw.print("ignoreCase=");
1216            pw.println(this.ignoreCase);
1217
1218            pw.print(subprefix);
1219            pw.println("Levels: [");
1220            for (Level level : this.levels) {
1221                level.print(pw, subsubprefix);
1222            }
1223            pw.print(subprefix);
1224            pw.println("]");
1225
1226            pw.print(subprefix);
1227            pw.println("Measures: [");
1228            for (Measure measure : this.measures) {
1229                measure.print(pw, subsubprefix);
1230            }
1231            pw.print(subprefix);
1232            pw.println("]");
1233        }
1234    }
1235
1236    static class NameTableDef extends ExplicitRules.TableDef {
1237        /**
1238         * Makes a NameTableDef from the catalog schema.
1239         */
1240        static ExplicitRules.NameTableDef make(
1241            final MondrianDef.AggName aggName,
1242            final ExplicitRules.Group group)
1243        {
1244            ExplicitRules.NameTableDef name =
1245                new ExplicitRules.NameTableDef(
1246                    aggName.getNameAttribute(),
1247                    aggName.getApproxRowCountAttribute(),
1248                    aggName.isIgnoreCase(),
1249                    group);
1250
1251            ExplicitRules.TableDef.add(name, aggName);
1252
1253            return name;
1254        }
1255
1256        private final String name;
1257
1258        public NameTableDef(
1259            final String name,
1260            final String approxRowCount,
1261            final boolean ignoreCase,
1262            final ExplicitRules.Group group)
1263        {
1264            super(ignoreCase, group);
1265            this.name = name;
1266            this.approxRowCount = loadApproxRowCount(approxRowCount);
1267        }
1268
1269        private int loadApproxRowCount(String approxRowCount) {
1270            boolean notNullAndNumeric =
1271                approxRowCount != null
1272                    && approxRowCount.matches("^\\d+$");
1273            if (notNullAndNumeric) {
1274                return Integer.parseInt(approxRowCount);
1275            } else {
1276                // if approxRowCount is not set, return MIN_VALUE to indicate
1277                return Integer.MIN_VALUE;
1278            }
1279        }
1280
1281        /**
1282         * Does the given tableName match this NameTableDef (either exact match
1283         * or, if set, a case insensitive match).
1284         */
1285        public boolean matches(final String tableName) {
1286            return (this.ignoreCase)
1287                ? this.name.equalsIgnoreCase(tableName)
1288                : this.name.equals(tableName);
1289        }
1290
1291        /**
1292         * Validate name and base class.
1293         */
1294        public void validate(final MessageRecorder msgRecorder) {
1295            msgRecorder.pushContextName("NameTableDef");
1296            try {
1297                checkAttributeString(msgRecorder, name, "name");
1298
1299                super.validate(msgRecorder);
1300            } finally {
1301                msgRecorder.popContextName();
1302            }
1303        }
1304
1305        public void print(final PrintWriter pw, final String prefix) {
1306            pw.print(prefix);
1307            pw.println("ExplicitRules.NameTableDef:");
1308            super.print(pw, prefix);
1309
1310            String subprefix = prefix + "  ";
1311
1312            pw.print(subprefix);
1313            pw.print("name=");
1314            pw.println(this.name);
1315        }
1316    }
1317
1318    /**
1319     * This class matches candidate aggregate table name with a pattern.
1320     */
1321    public static class PatternTableDef extends ExplicitRules.TableDef {
1322
1323        /**
1324         * Make a PatternTableDef from the catalog schema.
1325         */
1326        static ExplicitRules.PatternTableDef make(
1327            final MondrianDef.AggPattern aggPattern,
1328            final ExplicitRules.Group group)
1329        {
1330            ExplicitRules.PatternTableDef pattern =
1331                new ExplicitRules.PatternTableDef(
1332                    aggPattern.getPattern(),
1333                    aggPattern.isIgnoreCase(),
1334                    group);
1335
1336            MondrianDef.AggExclude[] excludes = aggPattern.getAggExcludes();
1337            if (excludes != null) {
1338                for (MondrianDef.AggExclude exclude1 : excludes) {
1339                    Exclude exclude = ExplicitRules.make(exclude1);
1340                    pattern.add(exclude);
1341                }
1342            }
1343
1344            ExplicitRules.TableDef.add(pattern, aggPattern);
1345
1346            return pattern;
1347        }
1348
1349        private final Pattern pattern;
1350        private List<Exclude> excludes;
1351
1352        public PatternTableDef(
1353            final String pattern,
1354            final boolean ignoreCase,
1355            final ExplicitRules.Group group)
1356        {
1357            super(ignoreCase, group);
1358            this.pattern = (this.ignoreCase)
1359                ? Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
1360                : Pattern.compile(pattern);
1361            this.excludes = Collections.emptyList();
1362        }
1363
1364        /**
1365         * Get the Pattern.
1366         */
1367        public Pattern getPattern() {
1368            return pattern;
1369        }
1370
1371        /**
1372         * Get an Iterator over the list of Excludes.
1373         */
1374        public List<Exclude> getExcludes() {
1375            return excludes;
1376        }
1377
1378        /**
1379         * Add an Exclude.
1380         */
1381        private void add(final Exclude exclude) {
1382            if (this.excludes == Collections.EMPTY_LIST) {
1383                this.excludes = new ArrayList<Exclude>();
1384            }
1385            this.excludes.add(exclude);
1386        }
1387
1388        /**
1389         * Return true if the tableName 1) matches the pattern and 2) is not
1390         * matched by any of the Excludes.
1391         */
1392        public boolean matches(final String tableName) {
1393            if (! pattern.matcher(tableName).matches()) {
1394                return false;
1395            } else {
1396                for (Exclude exclude : getExcludes()) {
1397                    if (exclude.isExcluded(tableName)) {
1398                        return false;
1399                    }
1400                }
1401                return true;
1402            }
1403        }
1404
1405        /**
1406         * Validate excludes and base class.
1407         */
1408        public void validate(final MessageRecorder msgRecorder) {
1409            msgRecorder.pushContextName("PatternTableDef");
1410            try {
1411                checkAttributeString(msgRecorder, pattern.pattern(), "pattern");
1412
1413                for (Exclude exclude : getExcludes()) {
1414                    exclude.validate(msgRecorder);
1415                }
1416                super.validate(msgRecorder);
1417            } finally {
1418                msgRecorder.popContextName();
1419            }
1420        }
1421
1422        public void print(final PrintWriter pw, final String prefix) {
1423            pw.print(prefix);
1424            pw.println("ExplicitRules.PatternTableDef:");
1425            super.print(pw, prefix);
1426
1427            String subprefix = prefix + "  ";
1428            String subsubprefix = subprefix + "  ";
1429
1430            pw.print(subprefix);
1431            pw.print("pattern=");
1432            pw.print(this.pattern.pattern());
1433            pw.print(":");
1434            pw.println(this.pattern.flags());
1435
1436            pw.print(subprefix);
1437            pw.println("Excludes: [");
1438            Iterator<Exclude> it = this.excludes.iterator();
1439            while (it.hasNext()) {
1440                Exclude exclude = it.next();
1441                exclude.print(pw, subsubprefix);
1442            }
1443            pw.print(subprefix);
1444            pw.println("]");
1445        }
1446    }
1447
1448    /**
1449     * Helper method used to determine if an attribute with name attrName has a
1450     * non-empty value.
1451     */
1452    private static void checkAttributeString(
1453        final MessageRecorder msgRecorder,
1454        final String attrValue,
1455        final String attrName)
1456    {
1457        if (attrValue == null) {
1458            msgRecorder.reportError(mres.NullAttributeString.str(
1459                msgRecorder.getContext(),
1460                attrName));
1461        } else if (attrValue.length() == 0) {
1462            msgRecorder.reportError(mres.EmptyAttributeString.str(
1463                msgRecorder.getContext(),
1464                attrName));
1465        }
1466    }
1467
1468
1469    private ExplicitRules() {
1470    }
1471}
1472
1473// End ExplicitRules.java