001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2005-2005 Julian Hyde
008// Copyright (C) 2005-2012 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap.aggmatcher;
012
013import mondrian.olap.*;
014import mondrian.recorder.MessageRecorder;
015import mondrian.rolap.*;
016import mondrian.rolap.aggmatcher.JdbcSchema.Table.Column;
017import mondrian.util.Pair;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.Iterator;
023import java.util.List;
024
025/**
026 * This is the Recognizer for the aggregate table descriptions that appear in
027 * the catalog schema files; the user explicitly defines the aggregate.
028 *
029 * @author Richard M. Emberson
030 */
031class ExplicitRecognizer extends Recognizer {
032    private ExplicitRules.TableDef tableDef;
033    private RolapCube cube;
034
035    ExplicitRecognizer(
036        final ExplicitRules.TableDef tableDef,
037        final RolapStar star,
038        RolapCube cube,
039        final JdbcSchema.Table dbFactTable,
040        final JdbcSchema.Table aggTable,
041        final MessageRecorder msgRecorder)
042    {
043        super(star, dbFactTable, aggTable, msgRecorder);
044        this.tableDef = tableDef;
045        this.cube = cube;
046    }
047
048    /**
049     * Get the ExplicitRules.TableDef associated with this instance.
050     */
051    protected ExplicitRules.TableDef getTableDef() {
052        return tableDef;
053    }
054
055    /**
056     * Get the Matcher to be used to match columns to be ignored.
057     */
058    protected Recognizer.Matcher getIgnoreMatcher() {
059        return getTableDef().getIgnoreMatcher();
060    }
061
062    /**
063     * Get the Matcher to be used to match the column which is the fact count
064     * column.
065     */
066    protected Recognizer.Matcher getFactCountMatcher() {
067        return getTableDef().getFactCountMatcher();
068    }
069
070    /**
071     * Make the measures for this aggregate table.
072     * <p>
073     * First, iterate through all of the columns in the table.
074     * For each column, iterate through all of the tableDef measures, the
075     * explicit definitions of a measure.
076     * If the table's column name matches the column name in the measure
077     * definition, then make a measure.
078     * Next, look through all of the fact table column usage measures.
079     * For each such measure usage that has a sibling foreign key usage
080     * see if the tableDef has a foreign key defined with the same name.
081     * If so, then, for free, we can make a measure for the aggregate using
082     * its foreign key.
083     * <p>
084     *
085     * @return number of measures created.
086     */
087    protected int checkMeasures() {
088        msgRecorder.pushContextName("ExplicitRecognizer.checkMeasures");
089        try {
090            int measureColumnCounts = 0;
091            // Look at each aggregate table column. For each measure defined,
092            // see if the measure's column name equals the column's name.
093            // If so, make the aggregate measure usage for that column.
094            for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) {
095                // if marked as ignore, then do not consider
096                if (aggColumn.hasUsage(JdbcSchema.UsageType.IGNORE)) {
097                    continue;
098                }
099
100                String aggColumnName = aggColumn.getName();
101
102                for (ExplicitRules.TableDef.Measure measure
103                    : getTableDef().getMeasures())
104                {
105                    // Column name match is case insensitive
106                    if (measure.getColumnName().equalsIgnoreCase(aggColumnName))
107                    {
108                        String name = measure.getName();
109                        List<Id.Segment> parts = Util.parseIdentifier(name);
110                        Id.Segment nameLast = Util.last(parts);
111
112                        RolapStar.Measure m = null;
113                        if (nameLast instanceof Id.NameSegment) {
114                            m = star.getFactTable().lookupMeasureByName(
115                                cube.getName(),
116                                ((Id.NameSegment) nameLast).name);
117                        }
118                        RolapAggregator agg = null;
119                        if (m != null) {
120                            agg = m.getAggregator();
121                        }
122                        // Ok, got a match, so now make a measure
123                        makeMeasure(measure, agg, aggColumn);
124                        measureColumnCounts++;
125                    }
126                }
127            }
128            // Ok, now look at all of the fact table columns with measure usage
129            // that have a sibling foreign key usage. These can be automagically
130            // generated for the aggregate table as long as it still has the
131            // foreign key.
132            for (Iterator<JdbcSchema.Table.Column.Usage> it =
133                     dbFactTable.getColumnUsages(JdbcSchema.UsageType.MEASURE);
134                 it.hasNext();)
135            {
136                JdbcSchema.Table.Column.Usage factUsage = it.next();
137                JdbcSchema.Table.Column factColumn = factUsage.getColumn();
138
139                if (factColumn.hasUsage(JdbcSchema.UsageType.FOREIGN_KEY)) {
140                    // What we've got here is a measure based upon a foreign key
141                    String aggFK =
142                        getTableDef().getAggregateFK(factColumn.getName());
143                    // OK, not a lost dimension
144                    if (aggFK != null) {
145                        JdbcSchema.Table.Column aggColumn =
146                            aggTable.getColumn(aggFK);
147
148                        // Column name match is case insensitive
149                        if (aggColumn == null) {
150                            aggColumn = aggTable.getColumn(aggFK.toLowerCase());
151                        }
152                        if (aggColumn == null) {
153                            aggColumn = aggTable.getColumn(aggFK.toUpperCase());
154                        }
155
156                        if (aggColumn != null) {
157                            makeMeasure(factUsage, aggColumn);
158                            measureColumnCounts++;
159                        }
160                    }
161                }
162            }
163            return measureColumnCounts;
164        } finally {
165            msgRecorder.popContextName();
166        }
167    }
168
169    /**
170     * Make a measure. This makes a measure usage using the Aggregator found in
171     * the RolapStar.Measure associated with the ExplicitRules.TableDef.Measure.
172     */
173    protected void makeMeasure(
174        final ExplicitRules.TableDef.Measure measure,
175        RolapAggregator factAgg,
176        final JdbcSchema.Table.Column aggColumn)
177    {
178        RolapStar.Measure rm = measure.getRolapStarMeasure();
179
180        JdbcSchema.Table.Column.Usage aggUsage =
181            aggColumn.newUsage(JdbcSchema.UsageType.MEASURE);
182
183        aggUsage.setSymbolicName(measure.getSymbolicName());
184        RolapAggregator ra = (factAgg == null)
185                    ? convertAggregator(aggUsage, rm.getAggregator())
186                    : convertAggregator(aggUsage, factAgg, rm.getAggregator());
187        aggUsage.setAggregator(ra);
188
189        aggUsage.rMeasure = rm;
190    }
191
192    /**
193     * Creates a foreign key usage.
194     *
195     * <p> First the column name of the fact usage which is a foreign key is
196     * used to search for a foreign key definition in the
197     * ExplicitRules.tableDef.  If not found, thats ok, it is just a lost
198     * dimension.  If found, look for a column in the aggregate table with that
199     * name and make a foreign key usage.
200     */
201    protected int matchForeignKey(
202        final JdbcSchema.Table.Column.Usage factUsage)
203    {
204        JdbcSchema.Table.Column factColumn = factUsage.getColumn();
205        String aggFK = getTableDef().getAggregateFK(factColumn.getName());
206
207        // OK, a lost dimension
208        if (aggFK == null) {
209            return 0;
210        }
211
212        int matchCount = 0;
213        for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) {
214            // if marked as ignore, then do not consider
215            if (aggColumn.hasUsage(JdbcSchema.UsageType.IGNORE)) {
216                continue;
217            }
218
219            if (aggFK.equals(aggColumn.getName())) {
220                makeForeignKey(factUsage, aggColumn, aggFK);
221                matchCount++;
222            }
223        }
224        return matchCount;
225    }
226
227    /**
228     * Creates a level usage. A level usage is a column that is used in a
229     * collapsed dimension aggregate table.
230     *
231     * <p> First, iterate through the ExplicitRules.TableDef's level
232     * definitions for one with a name equal to the RolapLevel unique name,
233     * i.e., [Time].[Quarter].  Now, using the level's column name, search
234     * through the aggregate table's columns for one with that name and make a
235     * level usage for the column.
236     */
237    protected void matchLevels(
238        final Hierarchy hierarchy,
239        final HierarchyUsage hierarchyUsage)
240    {
241        msgRecorder.pushContextName("ExplicitRecognizer.matchLevel");
242        try {
243            // Try to match a Level's name against the RolapLevel
244            // unique name.
245            List<Pair<RolapLevel, JdbcSchema.Table.Column>> levelMatches =
246                new ArrayList<Pair<RolapLevel, JdbcSchema.Table.Column>>();
247            List<ExplicitRules.TableDef.Level> aggLevels =
248                new ArrayList<ExplicitRules.TableDef.Level>();
249            level_loop:
250            for (Level hLevel : hierarchy.getLevels()) {
251                if (hLevel.isAll()) {
252                    continue;
253                }
254                final RolapLevel rLevel = (RolapLevel) hLevel;
255                String levelUniqueName = rLevel.getUniqueName();
256                for (ExplicitRules.TableDef.Level level
257                    : getTableDef().getLevels())
258                {
259                    if (level.getName().equals(levelUniqueName)) {
260                        // Now can we find a column in the aggTable
261                        // that matches the Level's column
262                        final String columnName = level.getColumnName();
263                        for (JdbcSchema.Table.Column aggColumn
264                            : aggTable.getColumns())
265                        {
266                            if (aggColumn.getName()
267                                .equalsIgnoreCase(columnName))
268                            {
269                                levelMatches.add(
270                                    new Pair<RolapLevel,
271                                        JdbcSchema.Table.Column>(
272                                            rLevel, aggColumn));
273                                aggLevels.add(level);
274                                continue level_loop;
275                            }
276                        }
277                    }
278                }
279            }
280            if (levelMatches.size() == 0) {
281                return;
282            }
283            // Sort the matches by level depth.
284            Collections.sort(
285                levelMatches,
286                new Comparator<Pair<RolapLevel, JdbcSchema.Table.Column>>() {
287                    public int compare(
288                        Pair<RolapLevel, Column> o1,
289                        Pair<RolapLevel, Column> o2)
290                    {
291                        return Util.compareIntegers(
292                            o1.left.getDepth(),
293                            o2.left.getDepth());
294                    }
295                });
296            Collections.sort(
297                aggLevels,
298                new Comparator<ExplicitRules.TableDef.Level>() {
299                    public int compare(
300                        mondrian.rolap.aggmatcher
301                            .ExplicitRules.TableDef.Level o1,
302                        mondrian.rolap.aggmatcher
303                            .ExplicitRules.TableDef.Level o2)
304                    {
305                        return Util.compareIntegers(
306                            o1.getRolapLevel().getDepth(),
307                            o2.getRolapLevel().getDepth());
308                    }
309                });
310            // Validate by iterating.
311            boolean forceCollapse = false;
312            for (Pair<RolapLevel, JdbcSchema.Table.Column> pair
313                : levelMatches)
314            {
315                // Fail if the level is not the first match
316                // but the one before is not its parent.
317                if (levelMatches.indexOf(pair) > 0
318                    && pair.left.getDepth() - 1
319                        != levelMatches.get(
320                            levelMatches.indexOf(pair) - 1).left.getDepth())
321                {
322                    msgRecorder.reportError(
323                        "The aggregate table "
324                        + aggTable.getName()
325                        + " contains the column "
326                        + pair.right.getName()
327                        + " which maps to the level "
328                        + pair.left.getUniqueName()
329                        + " but its parent level is not part of that aggregation.");
330                }
331                // Warn if this level is marked as non-collapsed but the level
332                // above it is present in this agg table.
333                if (levelMatches.indexOf(pair) > 0
334                    && !aggLevels.get(levelMatches.indexOf(pair)).isCollapsed())
335                {
336                    forceCollapse = true;
337                    msgRecorder.reportWarning(
338                        "The aggregate table " + aggTable.getName()
339                        + " contains the column " + pair.right.getName()
340                        + " which maps to the level "
341                        + pair.left.getUniqueName()
342                        + " and is marked as non-collapsed, but its parent column is already present.");
343                }
344                // Fail if the level is the first, it isn't at the top,
345                // but it is marked as collapsed.
346                if (levelMatches.indexOf(pair) == 0
347                    && pair.left.getDepth() > 1
348                    && aggLevels.get(levelMatches.indexOf(pair)).isCollapsed())
349                {
350                    msgRecorder.reportError(
351                        "The aggregate table "
352                        + aggTable.getName()
353                        + " contains the column "
354                        + pair.right.getName()
355                        + " which maps to the level "
356                        + pair.left.getUniqueName()
357                        + " but its parent level is not part of that aggregation and this level is marked as collapsed.");
358                }
359                // Fail if the level is non-collapsed but its members
360                // are not unique.
361                if (!aggLevels.get(
362                        levelMatches.indexOf(pair)).isCollapsed()
363                            && !pair.left.isUnique())
364                {
365                    msgRecorder.reportError(
366                        "The aggregate table "
367                        + aggTable.getName()
368                        + " contains the column "
369                        + pair.right.getName()
370                        + " which maps to the level "
371                        + pair.left.getUniqueName()
372                        + " but that level doesn't have unique members and this level is marked as non collapsed.");
373                }
374            }
375            if (msgRecorder.hasErrors()) {
376                return;
377            }
378            // All checks out. Let's create the levels.
379            for (Pair<RolapLevel, JdbcSchema.Table.Column> pair
380                : levelMatches)
381            {
382                makeLevel(
383                    pair.right,
384                    hierarchy,
385                    hierarchyUsage,
386                    getColumnName(pair.left.getKeyExp()),
387                    aggLevels.get(levelMatches.indexOf(pair)).getColumnName(),
388                    pair.left.getName(),
389                    forceCollapse
390                        ? true
391                        : aggLevels.get(levelMatches.indexOf(pair))
392                            .isCollapsed(),
393                    pair.left);
394            }
395        } finally {
396            msgRecorder.popContextName();
397        }
398    }
399}
400
401// End ExplicitRecognizer.java