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