001    /*
002    // $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/Recognizer.java#22 $
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.Hierarchy;
014    import mondrian.olap.Dimension;
015    import mondrian.olap.MondrianDef;
016    import mondrian.resource.MondrianResource;
017    import mondrian.recorder.MessageRecorder;
018    import mondrian.rolap.RolapStar;
019    import mondrian.rolap.RolapLevel;
020    import mondrian.rolap.RolapSchema;
021    import mondrian.rolap.RolapCube;
022    import mondrian.rolap.RolapAggregator;
023    import mondrian.rolap.HierarchyUsage;
024    import mondrian.rolap.sql.SqlQuery;
025    
026    import java.util.*;
027    
028    import org.apache.log4j.Logger;
029    
030    /**
031     * Abstract Recognizer class used to determine if a candidate aggregate table
032     * has the column categories: "fact_count" column, measure columns, foreign key
033     * and level columns.
034     *
035     * <p>Derived classes use either the default or explicit column descriptions in
036     * matching column categories. The basic matching algorithm is in this class
037     * while some specific column category matching and column building must be
038     * specified in derived classes.
039     *
040     * <p>A Recognizer is created per candidate aggregate table. The tables columns are
041     * then categorized. All errors and warnings are added to a MessageRecorder.
042     *
043     * <p>This class is less about defining a type and more about code sharing.
044     *
045     * @author Richard M. Emberson
046     * @version $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/Recognizer.java#22 $
047     */
048    abstract class Recognizer {
049    
050        private static final MondrianResource mres = MondrianResource.instance();
051        private static final Logger LOGGER = Logger.getLogger(Recognizer.class);
052        /**
053         * This is used to wrap column name matching rules.
054         */
055        public interface Matcher {
056    
057            /**
058             * Return true it the name matches and false otherwise.
059             */
060            boolean matches(String name);
061        }
062    
063        protected final RolapStar star;
064        protected final JdbcSchema.Table dbFactTable;
065        protected final JdbcSchema.Table aggTable;
066        protected final MessageRecorder msgRecorder;
067        protected boolean returnValue;
068    
069        protected Recognizer(final RolapStar star,
070                             final JdbcSchema.Table dbFactTable,
071                             final JdbcSchema.Table aggTable,
072                             final MessageRecorder msgRecorder) {
073            this.star = star;
074            this.dbFactTable = dbFactTable;
075            this.aggTable = aggTable;
076            this.msgRecorder = msgRecorder;
077    
078            returnValue = true;
079        }
080    
081        /**
082         * Return true if the candidate aggregate table was successfully mapped into
083         * the fact table. This is the top-level checking method.
084         * <p>
085         * It first checks the ignore columns.
086         * <p>
087         * Next, the existence of a fact count column is checked.
088         * <p>
089         * Then the measures are checked. First the specified (defined,
090         * explicit) measures are all determined. There must be at least one such
091         * measure. This if followed by checking for implied measures (e.g., if base
092         * fact table as both sum and average of a column and the aggregate has a
093         * sum measure, the there is an implied average measure in the aggregate).
094         * <p>
095         * Now the levels are checked. This is in two parts. First, foreign keys are
096         * checked followed by level columns (for collapsed dimension aggregates).
097         * <p>
098         * If eveything checks out, returns true.
099         */
100        public boolean check() {
101            checkIgnores();
102            checkFactCount();
103    
104            // Check measures
105            int nosMeasures = checkMeasures();
106            // There must be at least one measure
107            checkNosMeasures(nosMeasures);
108            generateImpliedMeasures();
109    
110            // Check levels
111            List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys = checkForeignKeys();
112    //printNotSeenForeignKeys(notSeenForeignKeys);
113            checkLevels(notSeenForeignKeys);
114    
115            if (returnValue) {
116                // Add all unused columns as warning to the MessageRecorder
117                checkUnusedColumns();
118            }
119    
120            return returnValue;
121        }
122    
123        /**
124         * Return the ignore column Matcher.
125         */
126        protected abstract Matcher getIgnoreMatcher();
127    
128        /**
129         * Check all columns to be marked as ignore.
130         */
131        protected void checkIgnores() {
132            Matcher ignoreMatcher = getIgnoreMatcher();
133    
134            for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) {
135                if (ignoreMatcher.matches(aggColumn.getName())) {
136                    makeIgnore(aggColumn);
137                }
138            }
139        }
140    
141        /**
142         * Create an ignore usage for the aggColumn.
143         *
144         * @param aggColumn
145         */
146        protected void makeIgnore(final JdbcSchema.Table.Column aggColumn) {
147            JdbcSchema.Table.Column.Usage usage =
148                aggColumn.newUsage(JdbcSchema.UsageType.IGNORE);
149            usage.setSymbolicName("Ignore");
150        }
151    
152    
153    
154        /**
155         * Return the fact count column Matcher.
156         */
157        protected abstract Matcher getFactCountMatcher();
158    
159        /**
160         * Make sure that the aggregate table has one fact count column and that its
161         * type is numeric.
162         */
163        protected void checkFactCount() {
164            msgRecorder.pushContextName("Recognizer.checkFactCount");
165    
166            try {
167    
168                Matcher factCountMatcher = getFactCountMatcher();
169    
170                int nosOfFactCounts = 0;
171                for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) {
172                    // if marked as ignore, then do not consider
173                    if (aggColumn.hasUsage(JdbcSchema.UsageType.IGNORE)) {
174                        continue;
175                    }
176                    if (factCountMatcher.matches(aggColumn.getName())) {
177                        if (aggColumn.getDatatype().isNumeric()) {
178                            makeFactCount(aggColumn);
179                            nosOfFactCounts++;
180                        } else {
181                            String msg = mres.NonNumericFactCountColumn.str(
182                                    aggTable.getName(),
183                                    dbFactTable.getName(),
184                                    aggColumn.getName(),
185                                    aggColumn.getTypeName());
186                            msgRecorder.reportError(msg);
187    
188                            returnValue = false;
189                        }
190                    }
191    
192                }
193                if (nosOfFactCounts == 0) {
194                    String msg = mres.NoFactCountColumns.str(
195                            aggTable.getName(),
196                            dbFactTable.getName());
197                    msgRecorder.reportError(msg);
198    
199                    returnValue = false;
200    
201                } else if (nosOfFactCounts > 1) {
202                    String msg = mres.TooManyFactCountColumns.str(
203                            aggTable.getName(),
204                            dbFactTable.getName(),
205                            nosOfFactCounts);
206                    msgRecorder.reportError(msg);
207    
208                    returnValue = false;
209                }
210    
211            } finally {
212                msgRecorder.popContextName();
213            }
214        }
215    
216    
217        /**
218         * Check all measure columns returning the number of measure columns.
219         */
220        protected abstract int checkMeasures();
221    
222        /**
223         * Create a fact count usage for the aggColumn.
224         *
225         * @param aggColumn
226         */
227        protected void makeFactCount(final JdbcSchema.Table.Column aggColumn) {
228            JdbcSchema.Table.Column.Usage usage =
229                aggColumn.newUsage(JdbcSchema.UsageType.FACT_COUNT);
230            usage.setSymbolicName("Fact Count");
231        }
232    
233    
234        /**
235         * Make sure there was at least one measure column identified.
236         *
237         * @param nosMeasures
238         */
239        protected void checkNosMeasures(int nosMeasures) {
240            msgRecorder.pushContextName("Recognizer.checkNosMeasures");
241    
242            try {
243                if (nosMeasures == 0) {
244                    String msg = mres.NoMeasureColumns.str(
245                            aggTable.getName(),
246                            dbFactTable.getName());
247                    msgRecorder.reportError(msg);
248    
249                    returnValue = false;
250                }
251    
252            } finally {
253                msgRecorder.popContextName();
254            }
255        }
256    
257        /**
258         * An implied measure in an aggregate table is one where there is both a sum
259         * and average measures in the base fact table and the aggregate table has
260         * either a sum or average, the other measure is implied and can be
261         * generated from the measure and the fact_count column.
262         * <p>
263         * For each column in the fact table, get its measure usages. If there is
264         * both an average and sum aggregator associated with the column, then
265         * iterator over all of the column usage of type measure of the aggregator
266         * table. If only one aggregate column usage measure is found and this
267         * RolapStar.Measure measure instance variable is the same as the
268         * the fact table's usage's instance variable, then the other measure is
269         * implied and the measure is created for the aggregate table.
270         */
271        protected void generateImpliedMeasures() {
272            for (JdbcSchema.Table.Column factColumn : aggTable.getColumns()) {
273                JdbcSchema.Table.Column.Usage sumFactUsage = null;
274                JdbcSchema.Table.Column.Usage avgFactUsage = null;
275    
276                for (Iterator<JdbcSchema.Table.Column.Usage> mit =
277                        factColumn.getUsages(JdbcSchema.UsageType.MEASURE);
278                        mit.hasNext();) {
279                    JdbcSchema.Table.Column.Usage factUsage = mit.next();
280                    if (factUsage.getAggregator() == RolapAggregator.Avg) {
281                        avgFactUsage = factUsage;
282                    } else if (factUsage.getAggregator() == RolapAggregator.Sum) {
283                        sumFactUsage = factUsage;
284                    }
285                }
286    
287                if (avgFactUsage != null && sumFactUsage != null) {
288                    JdbcSchema.Table.Column.Usage sumAggUsage = null;
289                    JdbcSchema.Table.Column.Usage avgAggUsage = null;
290                    int seenCount = 0;
291                    for (Iterator<JdbcSchema.Table.Column.Usage> mit =
292                        aggTable.getColumnUsages(JdbcSchema.UsageType.MEASURE);
293                            mit.hasNext();) {
294    
295                        JdbcSchema.Table.Column.Usage aggUsage = mit.next();
296                        if (aggUsage.rMeasure == avgFactUsage.rMeasure) {
297                            avgAggUsage = aggUsage;
298                            seenCount++;
299                        } else if (aggUsage.rMeasure == sumFactUsage.rMeasure) {
300                            sumAggUsage = aggUsage;
301                            seenCount++;
302                        }
303                    }
304                    if (seenCount == 1) {
305                        if (avgAggUsage != null) {
306                            makeMeasure(sumFactUsage, avgAggUsage);
307                        }
308                        if (sumAggUsage != null) {
309                            makeMeasure(avgFactUsage, sumAggUsage);
310                        }
311                    }
312                }
313            }
314        }
315    
316        /**
317         * Here we have the fact usage of either sum or avg and an aggregate usage
318         * of the opposite type. We wish to make a new aggregate usage based
319         * on the existing usage's column of the same type as the fact usage.
320         *
321         * @param factUsage fact usage
322         * @param aggSiblingUsage existing sibling usage
323         */
324        protected void makeMeasure(final JdbcSchema.Table.Column.Usage factUsage,
325                                   final JdbcSchema.Table.Column.Usage aggSiblingUsage) {
326            JdbcSchema.Table.Column aggColumn = aggSiblingUsage.getColumn();
327    
328            JdbcSchema.Table.Column.Usage aggUsage =
329                aggColumn.newUsage(JdbcSchema.UsageType.MEASURE);
330    
331            aggUsage.setSymbolicName(factUsage.getSymbolicName());
332            RolapAggregator ra = convertAggregator(
333                            aggUsage,
334                            factUsage.getAggregator(),
335                            aggSiblingUsage.getAggregator());
336            aggUsage.setAggregator(ra);
337            aggUsage.rMeasure = factUsage.rMeasure;
338        }
339    
340        /**
341         * This method creates an aggregate table column measure usage from a fact
342         * table column measure usage.
343         *
344         * @param factUsage
345         * @param aggColumn
346         */
347        protected void makeMeasure(final JdbcSchema.Table.Column.Usage factUsage,
348                                   final JdbcSchema.Table.Column aggColumn) {
349            JdbcSchema.Table.Column.Usage aggUsage =
350                aggColumn.newUsage(JdbcSchema.UsageType.MEASURE);
351    
352            aggUsage.setSymbolicName(factUsage.getSymbolicName());
353            RolapAggregator ra =
354                    convertAggregator(aggUsage, factUsage.getAggregator());
355            aggUsage.setAggregator(ra);
356            aggUsage.rMeasure = factUsage.rMeasure;
357        }
358    
359        /**
360         * This method determine how may aggregate table column's match the fact
361         * table foreign key column return in the number matched. For each matching
362         * column a foreign key usage is created.
363         */
364        protected abstract int matchForeignKey(JdbcSchema.Table.Column.Usage factUsage);
365    
366        /**
367         * This method checks the foreign key columns.
368         * <p>
369         * For each foreign key column usage in the fact table, determine how many
370         * aggregate table columns match that column usage. If there is more than
371         * one match, then that is an error. If there were no matches, then the
372         * foreign key usage is added to the list of fact column foreign key that
373         * were not in the aggregate table. This list is returned by this method.
374         * <p>
375         * This matches foreign keys that were not "lost" or "collapsed".
376         *
377         * @return  list on not seen foreign key column usages
378         */
379        protected List<JdbcSchema.Table.Column.Usage> checkForeignKeys() {
380            msgRecorder.pushContextName("Recognizer.checkForeignKeys");
381    
382            try {
383    
384                List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys =
385                    Collections.emptyList();
386    
387                for (Iterator<JdbcSchema.Table.Column.Usage> it =
388                    dbFactTable.getColumnUsages(JdbcSchema.UsageType.FOREIGN_KEY);
389                        it.hasNext();) {
390    
391                    JdbcSchema.Table.Column.Usage factUsage = it.next();
392    
393                    int matchCount = matchForeignKey(factUsage);
394    
395                    if (matchCount > 1) {
396                        String msg = mres.TooManyMatchingForeignKeyColumns.str(
397                                aggTable.getName(),
398                                dbFactTable.getName(),
399                                matchCount,
400                                factUsage.getColumn().getName());
401                        msgRecorder.reportError(msg);
402    
403                        returnValue = false;
404    
405                    } else if (matchCount == 0) {
406                        if (notSeenForeignKeys.isEmpty()) {
407                            notSeenForeignKeys = new ArrayList<JdbcSchema.Table.Column.Usage>();
408                        }
409                        notSeenForeignKeys.add(factUsage);
410                    }
411                }
412                return notSeenForeignKeys;
413    
414            } finally {
415                msgRecorder.popContextName();
416            }
417        }
418    
419        /**
420         * This method identifies those columns in the aggregate table that match
421         * "collapsed" dimension columns. Remember that a collapsed dimension is one
422         * where the higher levels of some hierarchy are columns in the aggregate
423         * table (and all of the lower levels are missing - it has aggregated up to
424         * the first existing level).
425         * <p>
426         * Here, we do not start from the fact table, we iterator over each cube.
427         * For each of the cube's dimensions, the dimension's hirarchies are
428         * iterated over. In turn, each hierarchy's usage is iterated over.
429         * if the hierarchy's usage's foreign key is not in the list of not seen
430         * foreign keys (the notSeenForeignKeys parameter), then that hierarchy is
431         * not considered. If the hierarchy's usage's foreign key is in the not seen
432         * list, then starting with the hierarchy's top level, it is determined if
433         * the combination of hierarchy, hierarchy usage, and level matches an
434         * aggregated table column. If so, then a level usage is created for that
435         * column and the hierarchy's next level is considered and so on until a
436         * for a level an aggregate table column does not match. Then we continue
437         * iterating over the hierarchy usages.
438         * <p>
439         * This check is different. The others mine the fact table usages. This
440         * looks through the fact table's cubes' dimension, hierarchy,
441         * hiearchy usages, levels to match up symbolic names for levels. The other
442         * checks match on "physical" characteristics, the column name; this matches
443         * on "logical" characteristics.
444         * <p>
445         * Note: Levels should not be created for foreign keys that WERE seen.
446         * Currently, this is NOT checked explicitly. For the explicit rules any
447         * extra columns MUST ge declared ignored or one gets an error.
448         *
449         * @param notSeenForeignKeys
450         */
451        protected void checkLevels(List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys) {
452    
453            // These are the factTable that do not appear in the aggTable.
454            // 1) find all cubes with this given factTable
455            // 1) per cube, find all usages with the column as foreign key
456            // 2) for each usage, find dimension and its levels
457            // 3) determine if level columns are represented
458    
459            // In generaly, there is only one cube.
460            for (RolapCube cube : findCubes()) {
461                Dimension[] dims = cube.getDimensions();
462                // start dimensions at 1 (0 is measures)
463                for (int j = 1; j < dims.length; j++) {
464                    Dimension dim = dims[j];
465                    // Ok, got dimension.
466                    // See if any of the levels exist as columns in the
467                    // aggTable. This requires applying a map from:
468                    //   hierarchyName
469                    //   levelName
470                    //   levelColumnName
471                    // to each "unassigned" column in the aggTable.
472                    // Remember that the rule is if a level does appear,
473                    // then all of the higher levels must also appear.
474                    String dimName = dim.getName();
475    
476                    Hierarchy[] hierarchies = dim.getHierarchies();
477                    for (Hierarchy hierarchy : hierarchies) {
478                        HierarchyUsage[] hierarchyUsages =
479                            cube.getUsages(hierarchy);
480                        for (HierarchyUsage hierarchyUsage : hierarchyUsages) {
481                            // Search through the notSeenForeignKeys list
482                            // making sure that this HierarchyUsage's
483                            // foreign key is not in the list.
484                            String foreignKey = hierarchyUsage.getForeignKey();
485                            boolean b = foreignKey == null ||
486                                inNotSeenForeignKeys(
487                                    foreignKey,
488                                    notSeenForeignKeys);
489                            if (!b) {
490                                // It was not in the not seen list, so ignore
491                                continue;
492                            }
493    
494    
495                            RolapLevel[] levels =
496                                (RolapLevel[]) hierarchy.getLevels();
497                            // If the top level is seen, then one or more
498                            // lower levels may appear but there can be no
499                            // missing levels between the top level and
500                            // lowest level seen.
501                            // On the other hand, if the top level is not
502                            // seen, then no other levels should be present.
503                            mid_level:
504                            for (RolapLevel level : levels) {
505                                if (level.isAll()) {
506                                    continue mid_level;
507                                }
508                                if (matchLevel(hierarchy, hierarchyUsage, level)) {
509    
510                                    continue mid_level;
511    
512                                } else {
513                                    // There were no matches, break
514                                    // For now, do not check lower levels
515                                    break mid_level;
516                                }
517                            }
518                        }
519                    }
520                }
521            }
522        }
523    
524        /**
525         * Return true if the foreignKey column name is in the list of not seen
526         * foreign keys.
527         */
528        boolean inNotSeenForeignKeys(
529            String foreignKey,
530            List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys)
531        {
532            for (JdbcSchema.Table.Column.Usage usage : notSeenForeignKeys) {
533                if (usage.getColumn().getName().equals(foreignKey)) {
534                    return true;
535                }
536            }
537            return false;
538        }
539    
540        /**
541         * Debug method: Print out not seen foreign key list.
542         *
543         * @param notSeenForeignKeys
544         */
545        private void printNotSeenForeignKeys(List notSeenForeignKeys) {
546            LOGGER.debug("Recognizer.printNotSeenForeignKeys: "
547                + aggTable.getName());
548            for (Iterator it = notSeenForeignKeys.iterator(); it.hasNext();) {
549                JdbcSchema.Table.Column.Usage usage =
550                    (JdbcSchema.Table.Column.Usage) it.next();
551                LOGGER.debug("  " + usage.getColumn().getName());
552            }
553        }
554    
555        /**
556         * Here a measure ussage is created and the right join condition is
557         * explicitly supplied. This is needed is when the aggregate table's column
558         * names may not match those found in the RolapStar.
559         *
560         * @param factUsage
561         * @param aggColumn
562         * @param rightJoinConditionColumnName
563         */
564        protected void makeForeignKey(final JdbcSchema.Table.Column.Usage factUsage,
565                                      final JdbcSchema.Table.Column aggColumn,
566                                      final String rightJoinConditionColumnName) {
567            JdbcSchema.Table.Column.Usage aggUsage =
568                aggColumn.newUsage(JdbcSchema.UsageType.FOREIGN_KEY);
569            aggUsage.setSymbolicName("FOREIGN_KEY");
570            // Extract from RolapStar enough stuff to build
571            // AggStar subtable except the column name of the right join
572            // condition might be different
573            aggUsage.rTable = factUsage.rTable;
574            aggUsage.rightJoinConditionColumnName = rightJoinConditionColumnName;
575    
576            aggUsage.rColumn = factUsage.rColumn;
577        }
578    
579        /**
580         * Match a aggregate table column given the hierarchy, hierarchy usage, and
581         * rolap level returning true if a match is found.
582         */
583        protected abstract boolean matchLevel(
584                final Hierarchy hierarchy,
585                final HierarchyUsage hierarchyUsage,
586                final RolapLevel level);
587    
588        /**
589         * Make a level column usage.
590         *
591         * <p> Note there is a check in this code. If a given aggregate table
592         * column has already has a level usage, then that usage must all refer to
593         * the same hierarchy usage join table and column name as the one that
594         * calling this method was to create. If there is an existing level usage
595         * for the column and it matches something else, then it is an error.
596         */
597        protected void makeLevel(
598                final JdbcSchema.Table.Column aggColumn,
599                final Hierarchy hierarchy,
600                final HierarchyUsage hierarchyUsage,
601                final String factColumnName,
602                final String levelColumnName,
603                final String symbolicName) {
604    
605            msgRecorder.pushContextName("Recognizer.makeLevel");
606    
607            try {
608    
609            if (aggColumn.hasUsage(JdbcSchema.UsageType.LEVEL)) {
610                // The column has at least one usage of level type
611                // make sure we are looking at the
612                // same table and column
613                for (Iterator<JdbcSchema.Table.Column.Usage> uit =
614                    aggColumn.getUsages(JdbcSchema.UsageType.LEVEL);
615                        uit.hasNext();) {
616                    JdbcSchema.Table.Column.Usage aggUsage = uit.next();
617    
618                    MondrianDef.Relation rel = hierarchyUsage.getJoinTable();
619                    String cName = levelColumnName;
620    
621                    if (! aggUsage.relation.equals(rel) ||
622                        ! aggColumn.column.name.equals(cName)) {
623    
624                        // this is an error so return
625                        String msg = mres.DoubleMatchForLevel.str(
626                            aggTable.getName(),
627                            dbFactTable.getName(),
628                            aggColumn.getName(),
629                            aggUsage.relation.toString(),
630                            aggColumn.column.name,
631                            rel.toString(),
632                            cName);
633                        msgRecorder.reportError(msg);
634    
635                        returnValue = false;
636    
637                        msgRecorder.throwRTException();
638                    }
639                }
640            } else {
641                JdbcSchema.Table.Column.Usage aggUsage =
642                    aggColumn.newUsage(JdbcSchema.UsageType.LEVEL);
643                // Cache table and column for the above
644                // check
645                aggUsage.relation = hierarchyUsage.getJoinTable();
646                aggUsage.joinExp = hierarchyUsage.getJoinExp();
647                aggUsage.levelColumnName = levelColumnName;
648    
649                aggUsage.setSymbolicName(symbolicName);
650    
651                String tableAlias;
652                if (aggUsage.joinExp instanceof MondrianDef.Column) {
653                    MondrianDef.Column mcolumn =
654                        (MondrianDef.Column) aggUsage.joinExp;
655                    tableAlias = mcolumn.table;
656                } else {
657                    tableAlias = aggUsage.relation.getAlias();
658                }
659    
660    
661                RolapStar.Table factTable = star.getFactTable();
662                RolapStar.Table descTable = factTable.findDescendant(tableAlias);
663    
664                if (descTable == null) {
665                    // TODO: what to do here???
666                    StringBuilder buf = new StringBuilder(256);
667                    buf.append("descendant table is null for factTable=");
668                    buf.append(factTable.getAlias());
669                    buf.append(", tableAlias=");
670                    buf.append(tableAlias);
671                    msgRecorder.reportError(buf.toString());
672    
673                    returnValue = false;
674    
675                    msgRecorder.throwRTException();
676                }
677    
678                RolapStar.Column rc = descTable.lookupColumn(factColumnName);
679    
680                if (rc == null) {
681                    rc = lookupInChildren(descTable, factColumnName);
682    
683                }
684                if (rc == null) {
685                    StringBuilder buf = new StringBuilder(256);
686                    buf.append("Rolap.Column not found (null) for tableAlias=");
687                    buf.append(tableAlias);
688                    buf.append(", factColumnName=");
689                    buf.append(factColumnName);
690                    buf.append(", levelColumnName=");
691                    buf.append(levelColumnName);
692                    buf.append(", symbolicName=");
693                    buf.append(symbolicName);
694                    msgRecorder.reportError(buf.toString());
695    
696                    returnValue = false;
697    
698                    msgRecorder.throwRTException();
699                } else {
700                    aggUsage.rColumn = rc;
701                }
702            }
703            } finally {
704                msgRecorder.popContextName();
705            }
706        }
707    
708        protected RolapStar.Column lookupInChildren(
709            final RolapStar.Table table,
710            final String factColumnName)
711        {
712            // This can happen if we are looking at a collapsed dimension
713            // table, and the collapsed dimension in question in the
714            // fact table is a snowflake (not just a star), so we
715            // must look deeper...
716            for (RolapStar.Table child : table.getChildren()) {
717                RolapStar.Column rc = child.lookupColumn(factColumnName);
718                if (rc != null) {
719                    return rc;
720                } else {
721                    rc = lookupInChildren(child, factColumnName);
722                    if (rc != null) {
723                        return rc;
724                    }
725                }
726            }
727            return null;
728        }
729    
730    
731        // Question: what if foreign key is seen, but there are also level
732        // columns - is this at least is a warning.
733    
734    
735        /**
736         * If everything is ok, issue warning for each aggTable column
737         * that has not been identified as a FACT_COLUMN, MEASURE_COLUMN or
738         * LEVEL_COLUMN.
739         */
740        protected void checkUnusedColumns() {
741            msgRecorder.pushContextName("Recognizer.checkUnusedColumns");
742            // Collection of messages for unused columns, sorted by column name
743            // so that tests are deterministic.
744            SortedMap<String, String> unusedColumnMsgs =
745                new TreeMap<String, String>();
746            for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) {
747                if (! aggColumn.hasUsage()) {
748                    String msg = mres.AggUnknownColumn.str(
749                        aggTable.getName(),
750                        dbFactTable.getName(),
751                        aggColumn.getName());
752                    unusedColumnMsgs.put(aggColumn.getName(), msg);
753                }
754            }
755            for (String msg : unusedColumnMsgs.values()) {
756                msgRecorder.reportWarning(msg);
757            }
758            msgRecorder.popContextName();
759        }
760    
761        /**
762         * Figure out what aggregator should be associated with a column usage.
763         * Generally, this aggregator is simply the RolapAggregator returned by
764         * calling the getRollup() method of the fact table column's
765         * RolapAggregator. But in the case that the fact table column's
766         * RolapAggregator is the "Avg" aggregator, then the special
767         * RolapAggregator.AvgFromSum is used.
768         * <p>
769         * Note: this code assumes that the aggregate table does not have an
770         * explicit average aggregation column.
771         *
772         * @param aggUsage
773         * @param factAgg
774         */
775        protected RolapAggregator convertAggregator(
776                final JdbcSchema.Table.Column.Usage aggUsage,
777                final RolapAggregator factAgg) {
778    
779            // NOTE: This assumes that the aggregate table does not have an explicit
780            // average column.
781            if (factAgg == RolapAggregator.Avg) {
782                String columnExpr = getFactCountExpr(aggUsage);
783                return new RolapAggregator.AvgFromSum(columnExpr);
784            } else if (factAgg == RolapAggregator.DistinctCount) {
785                //return RolapAggregator.Count;
786                return RolapAggregator.DistinctCount;
787            } else {
788                return factAgg;
789            }
790        }
791    
792        /**
793         * The method chooses a special aggregator for the aggregate table column's
794         * usage.
795         * <pre>
796         * If the fact table column's aggregator was "Avg":
797         *   then if the sibling aggregator was "Avg":
798         *      the new aggregator is RolapAggregator.AvgFromAvg
799         *   else if the sibling aggregator was "Sum":
800         *      the new aggregator is RolapAggregator.AvgFromSum
801         * else if the fact table column's aggregator was "Sum":
802         *   if the sibling aggregator was "Avg":
803         *      the new aggregator is RolapAggregator.SumFromAvg
804         * </pre>
805         * Note that there is no SumFromSum since that is not a special case
806         * requiring a special aggregator.
807         * <p>
808         * if no new aggregator was selected, then the fact table's aggregator
809         * rollup aggregator is used.
810         *
811         * @param aggUsage
812         * @param factAgg
813         * @param siblingAgg
814         */
815        protected RolapAggregator convertAggregator(
816                final JdbcSchema.Table.Column.Usage aggUsage,
817                final RolapAggregator factAgg,
818                final RolapAggregator siblingAgg) {
819    
820            msgRecorder.pushContextName("Recognizer.convertAggregator");
821            RolapAggregator rollupAgg =  null;
822    
823            String columnExpr = getFactCountExpr(aggUsage);
824            if (factAgg == RolapAggregator.Avg) {
825                if (siblingAgg == RolapAggregator.Avg) {
826                    rollupAgg =  new RolapAggregator.AvgFromAvg(columnExpr);
827                } else if (siblingAgg == RolapAggregator.Sum) {
828                    rollupAgg =  new RolapAggregator.AvgFromSum(columnExpr);
829                }
830            } else if (factAgg == RolapAggregator.Sum) {
831                if (siblingAgg == RolapAggregator.Avg) {
832                    rollupAgg =  new RolapAggregator.SumFromAvg(columnExpr);
833                } else if (siblingAgg instanceof RolapAggregator.AvgFromAvg) {
834                    // needed for BUG_1541077.testTotalAmount
835                    rollupAgg =  new RolapAggregator.SumFromAvg(columnExpr);
836                }
837            }
838    
839            if (rollupAgg == null) {
840                rollupAgg = (RolapAggregator) factAgg.getRollup();
841            }
842    
843            if (rollupAgg == null) {
844                String msg = mres.NoAggregatorFound.str(
845                    aggUsage.getSymbolicName(),
846                    factAgg.getName(),
847                    siblingAgg.getName());
848                msgRecorder.reportError(msg);
849            }
850    
851            msgRecorder.popContextName();
852            return rollupAgg;
853        }
854    
855        /**
856         * Given an aggregate table column usage, find the column name of the
857         * table's fact count column usage.
858         *
859         * @param aggUsage Aggregate table column usage
860         * @return The name of the column which holds the fact count.
861         */
862        private String getFactCountExpr(final JdbcSchema.Table.Column.Usage aggUsage) {
863            // get the fact count column name.
864            JdbcSchema.Table aggTable = aggUsage.getColumn().getTable();
865    
866            // iterator over fact count usages - in the end there can be only one!!
867            Iterator<JdbcSchema.Table.Column.Usage> it =
868                aggTable.getColumnUsages(JdbcSchema.UsageType.FACT_COUNT);
869            it.hasNext();
870            JdbcSchema.Table.Column.Usage usage = it.next();
871    
872            // get the columns name
873            String factCountColumnName = usage.getColumn().getName();
874            String tableName = aggTable.getName();
875    
876            // we want the fact count expression
877            MondrianDef.Column column =
878                new MondrianDef.Column(tableName, factCountColumnName);
879            SqlQuery sqlQuery = star.getSqlQuery();
880            return column.getExpression(sqlQuery);
881        }
882    
883        /**
884         * Finds all cubes that use this fact table.
885         */
886        protected List<RolapCube> findCubes() {
887            String name = dbFactTable.getName();
888    
889            List<RolapCube> list = new ArrayList<RolapCube>();
890            RolapSchema schema = star.getSchema();
891            for (RolapCube cube : schema.getCubeList()) {
892                if (cube.isVirtual()) {
893                    continue;
894                }
895                RolapStar cubeStar = cube.getStar();
896                String factTableName = cubeStar.getFactTable().getAlias();
897                if (name.equals(factTableName)) {
898                    list.add(cube);
899                }
900            }
901            return list;
902        }
903    
904        /**
905         * Given a {@link mondrian.olap.MondrianDef.Expression}, returns
906         * the associated column name.
907         *
908         * <p>Note: if the {@link mondrian.olap.MondrianDef.Expression} is
909         * not a {@link mondrian.olap.MondrianDef.Column} or {@link
910         * mondrian.olap.MondrianDef.KeyExpression}, returns null. This
911         * will result in an error.
912         */
913        protected String getColumnName(MondrianDef.Expression expr) {
914            msgRecorder.pushContextName("Recognizer.getColumnName");
915    
916            try {
917                if (expr instanceof MondrianDef.Column) {
918                    MondrianDef.Column column = (MondrianDef.Column) expr;
919                    return column.getColumnName();
920                } else if (expr instanceof MondrianDef.KeyExpression) {
921                    MondrianDef.KeyExpression key = (MondrianDef.KeyExpression) expr;
922                    return key.toString();
923                }
924    
925                String msg = mres.NoColumnNameFromExpression.str(
926                    expr.toString());
927                msgRecorder.reportError(msg);
928    
929                return null;
930    
931            } finally {
932                msgRecorder.popContextName();
933            }
934        }
935    }
936    
937    // End Recognizer.java