001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2005-2005 Julian Hyde
008// Copyright (C) 2005-2009 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap.aggmatcher;
012
013import mondrian.olap.Hierarchy;
014import mondrian.olap.Level;
015import mondrian.recorder.MessageRecorder;
016import mondrian.resource.MondrianResource;
017import mondrian.rolap.*;
018import mondrian.rolap.aggmatcher.JdbcSchema.Table.Column;
019import mondrian.util.Pair;
020
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.Iterator;
025import java.util.List;
026
027/**
028 * This is the default Recognizer. It uses the rules found in the file
029 * DefaultRules.xml to find aggregate tables and there columns.
030 *
031 * @author Richard M. Emberson
032 */
033class DefaultRecognizer extends Recognizer {
034
035    private static final MondrianResource mres = MondrianResource.instance();
036
037    private final DefaultRules aggDefault;
038
039    DefaultRecognizer(
040        final DefaultRules aggDefault,
041        final RolapStar star,
042        final JdbcSchema.Table dbFactTable,
043        final JdbcSchema.Table aggTable,
044        final MessageRecorder msgRecorder)
045    {
046        super(star, dbFactTable, aggTable, msgRecorder);
047        this.aggDefault = aggDefault;
048    }
049
050    /**
051     * Get the DefaultRules instance associated with this object.
052     */
053    DefaultRules getRules() {
054        return aggDefault;
055    }
056
057    /**
058     * Get the Matcher to be used to match columns to be ignored.
059     */
060    protected Recognizer.Matcher getIgnoreMatcher() {
061        return getRules().getIgnoreMatcher();
062    }
063
064    /**
065     * Get the Matcher to be used to match the column which is the fact count
066     * column.
067     */
068    protected Recognizer.Matcher getFactCountMatcher() {
069        return getRules().getFactCountMatcher();
070    }
071
072    /**
073     * Get the Match used to identify columns that are measures.
074     */
075    protected Recognizer.Matcher getMeasureMatcher(
076        JdbcSchema.Table.Column.Usage factUsage)
077    {
078        String measureName = factUsage.getSymbolicName();
079        String measureColumnName = factUsage.getColumn().getName();
080        String aggregateName = factUsage.getAggregator().getName();
081
082        return getRules().getMeasureMatcher(
083            measureName,
084            measureColumnName,
085            aggregateName);
086    }
087
088    /**
089     * Create measures for an aggregate table.
090     * <p>
091     * First, iterator through all fact table measure usages.
092     * Create a Matcher for each such usage.
093     * Iterate through all aggregate table columns.
094     * For each column that matches create a measure usage.
095     * <p>
096     * Per fact table measure usage, at most only one aggregate measure should
097     * be created.
098     *
099     * @return number of measures created.
100     */
101    protected int checkMeasures() {
102        msgRecorder.pushContextName("DefaultRecognizer.checkMeasures");
103
104        try {
105            int measureCountCount = 0;
106
107            for (Iterator<JdbcSchema.Table.Column.Usage> it =
108                     dbFactTable.getColumnUsages(JdbcSchema.UsageType.MEASURE);
109                it.hasNext();)
110            {
111                JdbcSchema.Table.Column.Usage factUsage = it.next();
112
113                Matcher matcher = getMeasureMatcher(factUsage);
114
115                int matchCount = 0;
116                for (JdbcSchema.Table.Column aggColumn
117                    : aggTable.getColumns())
118                {
119                    // if marked as ignore, then do not consider
120                    if (aggColumn.hasUsage(JdbcSchema.UsageType.IGNORE)) {
121                        continue;
122                    }
123
124                    if (matcher.matches(aggColumn.getName())) {
125                        makeMeasure(factUsage, aggColumn);
126
127                        measureCountCount++;
128                        matchCount++;
129                    }
130                }
131
132                if (matchCount > 1) {
133                    String msg = mres.AggMultipleMatchingMeasure.str(
134                        msgRecorder.getContext(),
135                        aggTable.getName(),
136                        dbFactTable.getName(),
137                        matchCount,
138                        factUsage.getSymbolicName(),
139                        factUsage.getColumn().getName(),
140                        factUsage.getAggregator().getName());
141                    msgRecorder.reportError(msg);
142
143                    returnValue = false;
144                }
145            }
146            return measureCountCount;
147        } finally {
148            msgRecorder.popContextName();
149        }
150    }
151
152    /**
153     * This creates a foreign key usage.
154     *
155     * <p>Using the foreign key Matcher with the fact usage's column name the
156     * aggregate table's columns are searched for one that matches.  For each
157     * that matches a foreign key usage is created (thought if more than one is
158     * created its is an error which is handled in the calling code.
159     */
160    protected int matchForeignKey(JdbcSchema.Table.Column.Usage factUsage) {
161        JdbcSchema.Table.Column factColumn = factUsage.getColumn();
162
163        // search to see if any of the aggTable's columns match
164        Recognizer.Matcher matcher =
165            getRules().getForeignKeyMatcher(factColumn.getName());
166
167        int matchCount = 0;
168        for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) {
169            // if marked as ignore, then do not consider
170            if (aggColumn.hasUsage(JdbcSchema.UsageType.IGNORE)) {
171                continue;
172            }
173
174            if (matcher.matches(aggColumn.getName())) {
175                makeForeignKey(factUsage, aggColumn, null);
176                matchCount++;
177            }
178        }
179        return matchCount;
180    }
181
182    /**
183     * Create level usages.
184     *
185     * <p> A Matcher is created using the Hierarchy's name, the RolapLevel
186     * name, and the column name associated with the RolapLevel's key
187     * expression.  The aggregate table columns are search for the first match
188     * and, if found, a level usage is created for that column.
189     */
190    protected void matchLevels(
191        final Hierarchy hierarchy,
192        final HierarchyUsage hierarchyUsage)
193    {
194        msgRecorder.pushContextName("DefaultRecognizer.matchLevel");
195        try {
196            List<Pair<RolapLevel, JdbcSchema.Table.Column>> levelMatches =
197                new ArrayList<Pair<RolapLevel, JdbcSchema.Table.Column>>();
198            level_loop:
199            for (Level level : hierarchy.getLevels()) {
200                if (level.isAll()) {
201                    continue;
202                }
203                final RolapLevel rLevel = (RolapLevel) level;
204
205                String usagePrefix = hierarchyUsage.getUsagePrefix();
206                String hierName = hierarchy.getName();
207                String levelName = rLevel.getName();
208                String levelColumnName = getColumnName(rLevel.getKeyExp());
209
210                Recognizer.Matcher matcher = getRules().getLevelMatcher(
211                    usagePrefix, hierName, levelName, levelColumnName);
212
213                for (JdbcSchema.Table.Column aggColumn
214                    : aggTable.getColumns())
215                {
216                    if (matcher.matches(aggColumn.getName())) {
217                        levelMatches.add(
218                            new Pair<RolapLevel,
219                                JdbcSchema.Table.Column>(
220                                    rLevel, aggColumn));
221                        continue level_loop;
222                    }
223                }
224            }
225            if (levelMatches.size() == 0) {
226                return;
227            }
228            // Sort the matches by level depth.
229            Collections.sort(
230                levelMatches,
231                new Comparator<Pair<RolapLevel, JdbcSchema.Table.Column>>() {
232                    public int compare(
233                        Pair<RolapLevel, Column> o1,
234                        Pair<RolapLevel, Column> o2)
235                    {
236                        return
237                            Integer.valueOf(o1.left.getDepth()).compareTo(
238                                Integer.valueOf(o2.left.getDepth()));
239                    }
240                });
241            // Validate by iterating.
242            for (Pair<RolapLevel, JdbcSchema.Table.Column> pair
243                : levelMatches)
244            {
245                boolean collapsed = true;
246                if (levelMatches.indexOf(pair) == 0
247                    && pair.left.getDepth() > 1)
248                {
249                    collapsed = false;
250                }
251                // Fail if the level is not the first match
252                // but the one before is not its parent.
253                if (levelMatches.indexOf(pair) > 0
254                    && pair.left.getDepth() - 1
255                        != levelMatches.get(
256                            levelMatches.indexOf(pair) - 1).left.getDepth())
257                {
258                    msgRecorder.reportError(
259                        "The aggregate table "
260                        + aggTable.getName()
261                        + " contains the column "
262                        + pair.right.getName()
263                        + " which maps to the level "
264                        + pair.left.getUniqueName()
265                        + " but its parent level is not part of that aggregation.");
266                }
267                // Fail if the level is non-collapsed but its members
268                // are not unique.
269                if (!collapsed
270                    && !pair.left.isUnique())
271                {
272                    msgRecorder.reportError(
273                        "The aggregate table "
274                        + aggTable.getName()
275                        + " contains the column "
276                        + pair.right.getName()
277                        + " which maps to the level "
278                        + pair.left.getUniqueName()
279                        + " but that level doesn't have unique members and this level is marked as non collapsed.");
280                }
281            }
282            if (msgRecorder.hasErrors()) {
283                return;
284            }
285            // All checks out. Let's create the levels.
286            for (Pair<RolapLevel, JdbcSchema.Table.Column> pair
287                : levelMatches)
288            {
289                boolean collapsed = true;
290                if (levelMatches.indexOf(pair) == 0
291                    && pair.left.getDepth() > 1)
292                {
293                    collapsed = false;
294                }
295                makeLevel(
296                    pair.right,
297                    hierarchy,
298                    hierarchyUsage,
299                    pair.right.column.name,
300                    getColumnName(pair.left.getKeyExp()),
301                    pair.left.getName(),
302                    collapsed,
303                    pair.left);
304            }
305        } finally {
306            msgRecorder.popContextName();
307        }
308    }
309}
310
311// End DefaultRecognizer.java