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-2011 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap.aggmatcher;
012
013import mondrian.olap.MondrianDef;
014import mondrian.olap.MondrianException;
015import mondrian.olap.MondrianProperties;
016import mondrian.olap.Util;
017import mondrian.recorder.ListRecorder;
018import mondrian.recorder.MessageRecorder;
019import mondrian.recorder.RecorderException;
020import mondrian.resource.MondrianResource;
021import mondrian.rolap.RolapCube;
022import mondrian.rolap.RolapSchema;
023import mondrian.rolap.RolapStar;
024
025import org.apache.log4j.Logger;
026
027import javax.sql.DataSource;
028
029import java.sql.SQLException;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.List;
033
034/**
035 * Manages aggregate tables.
036 *
037 * <p>It is used as follows:<ul>
038 * <li>A {@link mondrian.rolap.RolapSchema} creates an {@link AggTableManager},
039 *     and stores it in a member variable to ensure that it is not
040 *     garbage-collected.
041 * <li>The {@link mondrian.rolap.RolapSchema} calls {@link #initialize()},
042 *     which scans the JDBC catalog and identifies aggregate tables.
043 * <li>For each aggregate table, it creates an {@link AggStar} and calls
044 *     {@link RolapStar#addAggStar(AggStar)}.
045 *
046 * @author Richard M. Emberson
047 */
048public class AggTableManager {
049    private static final Logger LOGGER =
050        Logger.getLogger(AggTableManager.class);
051
052    private final RolapSchema schema;
053
054    private static final MondrianResource mres = MondrianResource.instance();
055
056    public AggTableManager(final RolapSchema schema) {
057        this.schema = schema;
058    }
059
060    /**
061     * This should ONLY be called if the AggTableManager is no longer going
062     * to be used. In fact, it should only be called indirectly by its
063     * associated RolapSchema object.
064     */
065    public void finalCleanUp() {
066        removeJdbcSchema();
067
068        if (getLogger().isDebugEnabled()) {
069            getLogger().debug(
070                "AggTableManager.finalCleanUp: schema="
071                    + schema.getName());
072        }
073    }
074
075    /**
076     * Get the Logger.
077     */
078    public Logger getLogger() {
079        return LOGGER;
080    }
081
082    /**
083     * Initializes this object, loading all aggregate tables and associating
084     * them with {@link RolapStar}s.
085     * This method should only be called once.
086     */
087    public void initialize() {
088        if (MondrianProperties.instance().ReadAggregates.get()) {
089            try {
090                loadRolapStarAggregates();
091            } catch (SQLException ex) {
092                throw mres.AggLoadingError.ex(ex);
093            }
094        }
095        printResults();
096    }
097
098    private void printResults() {
099/*
100 *   This was too much information at the INFO level, compared to the
101 *   rest of Mondrian
102 *
103 *         if (getLogger().isInfoEnabled()) {
104            // print just Star table alias and AggStar table names
105            StringBuilder buf = new StringBuilder(1024);
106            buf.append(Util.nl);
107            for (Iterator it = getStars(); it.hasNext();) {
108                RolapStar star = (RolapStar) it.next();
109                buf.append(star.getFactTable().getAlias());
110                buf.append(Util.nl);
111                for (Iterator ait = star.getAggStars(); ait.hasNext();) {
112                    AggStar aggStar = (AggStar) ait.next();
113                    buf.append("    ");
114                    buf.append(aggStar.getFactTable().getName());
115                    buf.append(Util.nl);
116                }
117            }
118            getLogger().info(buf.toString());
119
120        } else
121*/
122        if (getLogger().isDebugEnabled()) {
123            // print everything, Star, subTables, AggStar and subTables
124            // could be a lot
125            StringBuilder buf = new StringBuilder(4096);
126            buf.append(Util.nl);
127            for (RolapStar star : getStars()) {
128                buf.append(star.toString());
129                buf.append(Util.nl);
130            }
131            getLogger().debug(buf.toString());
132        }
133    }
134
135    private JdbcSchema getJdbcSchema() {
136        DataSource dataSource = schema.getInternalConnection().getDataSource();
137
138        // This actually just does a lookup or simple constructor invocation,
139        // its not expected to fail
140        return JdbcSchema.makeDB(dataSource);
141    }
142
143    /**
144     * Remove the possibly already loaded snapshot of what is in the database.
145     */
146    private void removeJdbcSchema() {
147        DataSource dataSource = schema.getInternalConnection().getDataSource();
148        JdbcSchema.removeDB(dataSource);
149    }
150
151
152    /**
153     * This method loads and/or reloads the aggregate tables.
154     * <p>
155     * NOTE: At this point all RolapStars have been made for this
156     * schema (except for dynamically added cubes which I am going
157     * to ignore for right now). So, All stars have their columns
158     * and their BitKeys can be generated.
159     *
160     * @throws SQLException
161     */
162    private void loadRolapStarAggregates() throws SQLException {
163        ListRecorder msgRecorder = new ListRecorder();
164        try {
165            DefaultRules rules = DefaultRules.getInstance();
166            JdbcSchema db = getJdbcSchema();
167            // if we don't synchronize this on the db object,
168            // we may end up getting a Concurrency exception due to
169            // calls to other instances of AggTableManager.finalCleanUp()
170            synchronized (db) {
171                // fix for MONDRIAN-496
172                // flush any existing usages of the jdbc schema, so we
173                // don't accidentally use another star's metadata
174                db.flushUsages();
175
176                // loads tables, not their columns
177                db.load();
178
179                loop:
180                for (RolapStar star : getStars()) {
181                    // This removes any AggStars from any previous invocation of
182                    // this method (if any)
183                    star.prepareToLoadAggregates();
184
185                    List<ExplicitRules.Group> aggGroups = getAggGroups(star);
186                    for (ExplicitRules.Group group : aggGroups) {
187                        group.validate(msgRecorder);
188                    }
189
190                    String factTableName = star.getFactTable().getAlias();
191
192                    JdbcSchema.Table dbFactTable = db.getTable(factTableName);
193                    if (dbFactTable == null) {
194                        msgRecorder.reportWarning(
195                            "No Table found for fact name="
196                                + factTableName);
197                        continue loop;
198                    }
199
200                    // For each column in the dbFactTable, figure out it they
201                    // are measure or foreign key columns
202
203                    bindToStar(dbFactTable, star, msgRecorder);
204                    String schema = dbFactTable.table.schema;
205
206                    // Now look at all tables in the database and per table,
207                    // first see if it is a match for an aggregate table for
208                    // this fact table and second see if its columns match
209                    // foreign key and level columns.
210
211                    for (JdbcSchema.Table dbTable : db.getTables()) {
212                        String name = dbTable.getName();
213
214                        // Do the catalog schema aggregate excludes, exclude
215                        // this table name.
216                        if (ExplicitRules.excludeTable(name, aggGroups)) {
217                            continue;
218                        }
219
220                        // First see if there is an ExplicitRules match. If so,
221                        // then if all of the columns match up, then make an
222                        // AggStar. On the other hand, if there is no
223                        // ExplicitRules match, see if there is a Default
224                        // match. If so and if all the columns match up, then
225                        // also make an AggStar.
226                        ExplicitRules.TableDef tableDef =
227                            ExplicitRules.getIncludeByTableDef(name, aggGroups);
228
229                        boolean makeAggStar = false;
230                        int approxRowCount = Integer.MIN_VALUE;
231                        // Is it handled by the ExplicitRules
232                        if (tableDef != null) {
233                            // load columns
234                            dbTable.load();
235                            makeAggStar = tableDef.columnsOK(
236                                star,
237                                dbFactTable,
238                                dbTable,
239                                msgRecorder);
240                            approxRowCount = tableDef.getApproxRowCount();
241                        }
242                        if (! makeAggStar) {
243                            // Is it handled by the DefaultRules
244                            if (rules.matchesTableName(factTableName, name)) {
245                                // load columns
246                                dbTable.load();
247                                makeAggStar = rules.columnsOK(
248                                    star,
249                                    dbFactTable,
250                                    dbTable,
251                                    msgRecorder);
252                            }
253                        }
254
255                        if (makeAggStar) {
256                            dbTable.setTableUsageType(
257                                JdbcSchema.TableUsageType.AGG);
258                            dbTable.table = new MondrianDef.Table(
259                                schema,
260                                name,
261                                null, // null alias
262                                null); // don't know about table hints
263                            AggStar aggStar = AggStar.makeAggStar(
264                                star,
265                                dbTable,
266                                msgRecorder,
267                                approxRowCount);
268                            if (aggStar.getSize() > 0) {
269                                star.addAggStar(aggStar);
270                            } else {
271                                getLogger().warn(
272                                    mres.AggTableZeroSize.str(
273                                        aggStar.getFactTable().getName(),
274                                        factTableName));
275                            }
276                        }
277                        // Note: if the dbTable name matches but the columnsOK
278                        // does not, then this is an error and the aggregate
279                        // tables can not be loaded.
280                        // We do not "reset" the column usages in the dbTable
281                        // allowing it maybe to match another rule.
282                    }
283                }
284            }
285        } catch (RecorderException ex) {
286            throw new MondrianException(ex);
287        } finally {
288            msgRecorder.logInfoMessage(getLogger());
289            msgRecorder.logWarningMessage(getLogger());
290            msgRecorder.logErrorMessage(getLogger());
291            if (msgRecorder.hasErrors()) {
292                throw mres.AggLoadingExceededErrorCount.ex(
293                    msgRecorder.getErrorCount());
294            }
295        }
296    }
297
298    private Collection<RolapStar> getStars() {
299        return schema.getStars();
300    }
301
302    /**
303     * Returns a list containing every
304     * {@link mondrian.rolap.aggmatcher.ExplicitRules.Group} in every
305     * cubes in a given {@link RolapStar}.
306     */
307    protected List<ExplicitRules.Group> getAggGroups(RolapStar star) {
308        List<ExplicitRules.Group> aggGroups =
309            new ArrayList<ExplicitRules.Group>();
310        for (RolapCube cube : schema.getCubesWithStar(star)) {
311            if (cube.hasAggGroup() && cube.getAggGroup().hasRules()) {
312                aggGroups.add(cube.getAggGroup());
313            }
314        }
315        return aggGroups;
316    }
317
318    /**
319     * This method mines the RolapStar and annotes the JdbcSchema.Table
320     * dbFactTable by creating JdbcSchema.Table.Column.Usage instances. For
321     * example, a measure in the RolapStar becomes a measure usage for the
322     * column with the same name and a RolapStar foreign key column becomes a
323     * foreign key usage for the column with the same name.
324     *
325     * @param dbFactTable
326     * @param star
327     * @param msgRecorder
328     */
329    void bindToStar(
330        final JdbcSchema.Table dbFactTable,
331        final RolapStar star,
332        final MessageRecorder msgRecorder)
333        throws SQLException
334    {
335        msgRecorder.pushContextName("AggTableManager.bindToStar");
336        try {
337            // load columns
338            dbFactTable.load();
339
340            dbFactTable.setTableUsageType(JdbcSchema.TableUsageType.FACT);
341
342            MondrianDef.RelationOrJoin relation =
343                star.getFactTable().getRelation();
344            String schema = null;
345            MondrianDef.Hint[] tableHints = null;
346            if (relation instanceof MondrianDef.Table) {
347                schema = ((MondrianDef.Table) relation).schema;
348                tableHints = ((MondrianDef.Table) relation).tableHints;
349            }
350            String tableName = dbFactTable.getName();
351            String alias = null;
352            dbFactTable.table = new MondrianDef.Table(
353                schema,
354                tableName,
355                alias,
356                tableHints);
357
358            for (JdbcSchema.Table.Column factColumn
359                : dbFactTable.getColumns())
360            {
361                String cname = factColumn.getName();
362                RolapStar.Column[] rcs =
363                    star.getFactTable().lookupColumns(cname);
364
365                for (RolapStar.Column rc : rcs) {
366                    // its a measure
367                    if (rc instanceof RolapStar.Measure) {
368                        RolapStar.Measure rm = (RolapStar.Measure) rc;
369                        JdbcSchema.Table.Column.Usage usage =
370                            factColumn.newUsage(JdbcSchema.UsageType.MEASURE);
371                        usage.setSymbolicName(rm.getName());
372
373                        usage.setAggregator(rm.getAggregator());
374                        usage.rMeasure = rm;
375                    }
376                }
377
378                // it still might be a foreign key
379                RolapStar.Table rTable =
380                    star.getFactTable().findTableWithLeftJoinCondition(cname);
381                if (rTable != null) {
382                    JdbcSchema.Table.Column.Usage usage =
383                        factColumn.newUsage(JdbcSchema.UsageType.FOREIGN_KEY);
384                    usage.setSymbolicName("FOREIGN_KEY");
385                    usage.rTable = rTable;
386                } else {
387                    RolapStar.Column rColumn =
388                        star.getFactTable().lookupColumn(cname);
389                    if ((rColumn != null)
390                        && !(rColumn instanceof RolapStar.Measure))
391                    {
392                        // Ok, maybe its used in a non-shared dimension
393                        // This is a column in the fact table which is
394                        // (not necessarily) a measure but is also not
395                        // a foreign key to an external dimension table.
396                        JdbcSchema.Table.Column.Usage usage =
397                            factColumn.newUsage(
398                                JdbcSchema.UsageType.FOREIGN_KEY);
399                        usage.setSymbolicName("FOREIGN_KEY");
400                        usage.rColumn = rColumn;
401                    }
402                }
403
404                // warn if it has not been identified
405                if (!factColumn.hasUsage() && getLogger().isDebugEnabled()) {
406                    getLogger().debug(
407                        mres.UnknownFactTableColumn.str(
408                            msgRecorder.getContext(),
409                            dbFactTable.getName(),
410                            factColumn.getName()));
411                }
412            }
413        } finally {
414            msgRecorder.popContextName();
415        }
416    }
417}
418
419// End AggTableManager.java