001    /*
002    // $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/AggTableManager.java#26 $
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.*;
016    import mondrian.resource.MondrianResource;
017    
018    import org.apache.log4j.Logger;
019    import org.eigenbase.util.property.*;
020    import org.eigenbase.util.property.Property;
021    
022    import javax.sql.DataSource;
023    import java.util.*;
024    import java.sql.SQLException;
025    
026    /**
027     * Manages aggregate tables.
028     *
029     * <p>It is used as follows:<ul>
030     * <li>A {@link mondrian.rolap.RolapSchema} creates an {@link AggTableManager},
031     *     and stores it in a member variable to ensure that it is not
032     *     garbage-collected.
033     * <li>The {@link AggTableManager} creates and registers
034     *     {@link org.eigenbase.util.property.Trigger} objects, so that it is notified
035     *     when properties pertinent to aggregate tables change.
036     * <li>The {@link mondrian.rolap.RolapSchema} calls {@link #initialize()},
037     *     which scans the JDBC catalog and identifies aggregate tables.
038     * <li>For each aggregate table, it creates an {@link AggStar} and calls
039     *     {@link RolapStar#addAggStar(AggStar)}.
040     *
041     * @author Richard M. Emberson
042     * @version $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/AggTableManager.java#26 $
043     */
044    public class AggTableManager {
045        private static final Logger LOGGER =
046                Logger.getLogger(AggTableManager.class);
047    
048        private final RolapSchema schema;
049    
050        private static final MondrianResource mres = MondrianResource.instance();
051    
052        /**
053         * This is used to create forward references to triggers (so they do not
054         * get reaped until the RolapSchema is reaped).
055         */
056        private Trigger[] triggers;
057    
058        public AggTableManager(final RolapSchema schema) {
059            this.schema = schema;
060        }
061    
062        /**
063         * This should ONLY be called if the AggTableManager is no longer going
064         * to be used. In fact, it should only be called indirectly by its
065         * associated RolapSchema object.
066         */
067        public void finalCleanUp() {
068            removeJdbcSchema();
069            deregisterTriggers(MondrianProperties.instance());
070    
071            if (getLogger().isDebugEnabled()) {
072                StringBuilder buf = new StringBuilder(100);
073                buf.append("AggTableManager.finalCleanUp: schema=");
074                buf.append(schema.getName());
075                getLogger().debug(buf.toString());
076            }
077        }
078    
079        /**
080         * Get the Logger.
081         */
082        public Logger getLogger() {
083            return LOGGER;
084        }
085    
086        /**
087         * Initializes this object, loading all aggregate tables and associating
088         * them with {@link RolapStar}s.
089         * This method should only be called once.
090         */
091        public void initialize() {
092            if (MondrianProperties.instance().ReadAggregates.get()) {
093                try {
094                    loadRolapStarAggregates();
095    
096                } catch (SQLException ex) {
097                    throw mres.AggLoadingError.ex(ex);
098                }
099            }
100            registerTriggers();
101            printResults();
102        }
103        private void printResults() {
104    /*
105     *   This was too much information at the INFO level, compared to the
106     *   rest of Mondrian
107     *
108     *         if (getLogger().isInfoEnabled()) {
109                // print just Star table alias and AggStar table names
110                StringBuilder buf = new StringBuilder(1024);
111                buf.append(Util.nl);
112                for (Iterator it = getStars(); it.hasNext();) {
113                    RolapStar star = (RolapStar) it.next();
114                    buf.append(star.getFactTable().getAlias());
115                    buf.append(Util.nl);
116                    for (Iterator ait = star.getAggStars(); ait.hasNext();) {
117                        AggStar aggStar = (AggStar) ait.next();
118                        buf.append("    ");
119                        buf.append(aggStar.getFactTable().getName());
120                        buf.append(Util.nl);
121                    }
122                }
123                getLogger().info(buf.toString());
124    
125            } else
126    */
127            if (getLogger().isDebugEnabled()) {
128                // print everything, Star, subTables, AggStar and subTables
129                // could be a lot
130                StringBuilder buf = new StringBuilder(4096);
131                buf.append(Util.nl);
132                for (RolapStar star : getStars()) {
133                    buf.append(star.toString());
134                    buf.append(Util.nl);
135                }
136                getLogger().debug(buf.toString());
137            }
138        }
139        private void reLoadRolapStarAggregates() {
140            if (MondrianProperties.instance().ReadAggregates.get()) {
141                try {
142                    clearJdbcSchema();
143                    loadRolapStarAggregates();
144                    printResults();
145    
146                } catch (SQLException ex) {
147                    throw mres.AggLoadingError.ex(ex);
148                }
149            }
150        }
151    
152        private JdbcSchema getJdbcSchema() {
153            DataSource dataSource = schema.getInternalConnection().getDataSource();
154    
155            // This actually just does a lookup or simple constructor invocation,
156            // its not expected to fail
157            return JdbcSchema.makeDB(dataSource);
158        }
159    
160        /**
161         * Clear the possibly already loaded snapshot of what is in the database.
162         */
163        private void clearJdbcSchema() {
164            DataSource dataSource = schema.getInternalConnection().getDataSource();
165            JdbcSchema.clearDB(dataSource);
166        }
167    
168        /**
169         * Remove the possibly already loaded snapshot of what is in the database.
170         */
171        private void removeJdbcSchema() {
172            DataSource dataSource = schema.getInternalConnection().getDataSource();
173            JdbcSchema.removeDB(dataSource);
174        }
175    
176    
177        /**
178         * This method loads and/or reloads the aggregate tables.
179         * <p>
180         * NOTE: At this point all RolapStars have been made for this
181         * schema (except for dynamically added cubes which I am going
182         * to ignore for right now). So, All stars have their columns
183         * and their BitKeys can be generated.
184         *
185         * @throws SQLException
186         */
187        private void loadRolapStarAggregates() throws SQLException {
188            ListRecorder msgRecorder = new ListRecorder();
189            try {
190    
191            DefaultRules rules = DefaultRules.getInstance();
192            JdbcSchema db = getJdbcSchema();
193            // loads tables, not their columns
194            db.load();
195    
196            loop:
197            for (RolapStar star : getStars()) {
198                // This removes any AggStars from any previous invocation of this
199                // method (if any)
200                star.prepareToLoadAggregates();
201    
202                List<ExplicitRules.Group> aggGroups = getAggGroups(star);
203                for (ExplicitRules.Group group : aggGroups) {
204                    group.validate(msgRecorder);
205                }
206    
207    
208                String factTableName = star.getFactTable().getAlias();
209    
210                JdbcSchema.Table dbFactTable = db.getTable(factTableName);
211                if (dbFactTable == null) {
212                    msgRecorder.reportWarning("No Table found for fact name="
213                        +factTableName);
214    
215                    continue loop;
216                }
217    
218                // For each column in the dbFactTable, figure out it they are
219                // measure or foreign key columns
220                bindToStar(dbFactTable, star, msgRecorder);
221                String schema = dbFactTable.table.schema;
222    
223                // Now look at all tables in the database and per table, first see
224                // if it is a match for an aggregate table for this fact table and
225                // second see if its columns match foreign key and level columns.
226                for (JdbcSchema.Table dbTable : db.getTables()) {
227                    String name = dbTable.getName();
228    
229                    // Do the catalog schema aggregate excludes, exclude this
230                    // table name.
231                    if (ExplicitRules.excludeTable(name, aggGroups)) {
232                        continue;
233                    }
234    
235                    //
236                    // First see if there is an ExplicitRules match. If so, then if all
237                    // of the columns match up, then make an AggStar.
238                    // On the other hand, if there is no ExplicitRules match, see if
239                    // there is a Default match. If so and if all the columns
240                    // match up, then also make an AggStar.
241                    //
242                    ExplicitRules.TableDef tableDef =
243                        ExplicitRules.getIncludeByTableDef(name, aggGroups);
244    
245                    boolean makeAggStar = false;
246                    // Is it handled by the ExplicitRules
247                    if (tableDef != null) {
248                        // load columns
249                        dbTable.load();
250                        makeAggStar = tableDef.columnsOK(star,
251                                        dbFactTable,
252                                        dbTable,
253                                        msgRecorder);
254                    }
255                    if (! makeAggStar) {
256                        // Is it handled by the DefaultRules
257                        if (rules.matchesTableName(factTableName, name)) {
258                            // load columns
259                            dbTable.load();
260                            makeAggStar = rules.columnsOK(star,
261                                                dbFactTable,
262                                                dbTable,
263                                                msgRecorder);
264                        }
265                    }
266    
267    
268                    if (makeAggStar) {
269                        dbTable.setTableUsageType(JdbcSchema.TableUsageType.AGG);
270                        String alias = null;
271                        dbTable.table = new MondrianDef.Table(schema,
272                                                              name,
273                                                              alias);
274                        AggStar aggStar = AggStar.makeAggStar(star,
275                                                              dbTable,
276                                                              msgRecorder);
277                        if (aggStar.getSize() > 0) {
278                            star.addAggStar(aggStar);
279                        } else {
280                            String msg = mres.AggTableZeroSize.str(
281                                aggStar.getFactTable().getName(),
282                                factTableName);
283                            getLogger().warn(msg);
284                        }
285                    }
286                    // Note: if the dbTable name matches but the columnsOK does
287                    // not, then this is an error and the aggregate tables
288                    // can not be loaded.
289                    // We do not "reset" the column usages in the dbTable allowing
290                    // it maybe to match another rule.
291                }
292            }
293    
294            } catch (RecorderException ex) {
295                throw new MondrianException(ex);
296    
297            } finally {
298                msgRecorder.logInfoMessage(getLogger());
299                msgRecorder.logWarningMessage(getLogger());
300                msgRecorder.logErrorMessage(getLogger());
301                if (msgRecorder.hasErrors()) {
302                    throw mres.AggLoadingExceededErrorCount.ex(
303                        msgRecorder.getErrorCount());
304                }
305            }
306        }
307        private boolean runTrigger() {
308            if (RolapSchema.cacheContains(schema)) {
309                return true;
310            } else {
311                // must remove triggers
312                deregisterTriggers(MondrianProperties.instance());
313    
314                return false;
315            }
316    
317        }
318    
319        /**
320         * Registers triggers for the following properties:
321         * <ul>
322         *      <li>{@link MondrianProperties#ChooseAggregateByVolume}
323         *      <li>{@link MondrianProperties#AggregateRules}
324         *      <li>{@link MondrianProperties#AggregateRuleTag}
325         *      <li>{@link MondrianProperties#ReadAggregates}
326         * </ul>
327         */
328        private void registerTriggers() {
329            final MondrianProperties properties = MondrianProperties.instance();
330            triggers = new Trigger[] {
331    
332                // When the ordering AggStars property is changed, we must
333                // reorder them, so we create a trigger.
334                // There is no need to provide equals/hashCode methods for this
335                // Trigger since it is never explicitly removed.
336                new Trigger() {
337                    public boolean isPersistent() {
338                        return false;
339                    }
340                    public int phase() {
341                        return Trigger.SECONDARY_PHASE;
342                    }
343                    public void execute(Property property, String value) {
344                        if (AggTableManager.this.runTrigger()) {
345                            reOrderAggStarList();
346                        }
347                    }
348                },
349    
350                // Register to know when the Default resource/url has changed
351                // so that the default aggregate table recognition rules can
352                // be re-loaded.
353                // There is no need to provide equals/hashCode methods for this
354                // Trigger since it is never explicitly removed.
355                new Trigger() {
356                    public boolean isPersistent() {
357                        return false;
358                    }
359                    public int phase() {
360                        return Trigger.SECONDARY_PHASE;
361                    }
362                    public void execute(Property property, String value) {
363                        if (AggTableManager.this.runTrigger()) {
364                            reLoadRolapStarAggregates();
365                        }
366                    }
367                },
368    
369                // If the system started not using aggregates, i.e., the aggregate
370                // tables were not loaded, but then the property
371                // was changed to use aggregates, we must then load the aggregates
372                // if they were never loaded.
373                new Trigger() {
374                    public boolean isPersistent() {
375                        return false;
376                    }
377                    public int phase() {
378                        return Trigger.SECONDARY_PHASE;
379                    }
380                    public void execute(Property property, String value) {
381                        if (AggTableManager.this.runTrigger()) {
382                            reLoadRolapStarAggregates();
383                        }
384                    }
385                }
386            };
387    
388            // Note that for each AggTableManager theses triggers are
389            // added to the properties object. Each trigger has just
390            // been created and "knows" its AggTableManager instance.
391            // The triggers' hashCode and equals methods (those provided
392            // by the Object class) are used when removing the trigger.
393            properties.ChooseAggregateByVolume.addTrigger(triggers[0]);
394            properties.AggregateRules.addTrigger(triggers[1]);
395            properties.AggregateRuleTag.addTrigger(triggers[1]);
396            properties.ReadAggregates.addTrigger(triggers[2]);
397        }
398    
399        private void deregisterTriggers(final MondrianProperties properties) {
400            // Remove this AggTableManager's instance's triggers.
401            properties.ChooseAggregateByVolume.removeTrigger(triggers[0]);
402            properties.AggregateRules.removeTrigger(triggers[1]);
403            properties.AggregateRuleTag.removeTrigger(triggers[1]);
404            properties.ReadAggregates.removeTrigger(triggers[2]);
405        }
406    
407        private Collection<RolapStar> getStars() {
408            return schema.getStars();
409        }
410    
411        private void reOrderAggStarList() {
412            for (RolapStar star : getStars()) {
413                star.reOrderAggStarList();
414            }
415        }
416    
417        /**
418         * Returns a list containing every
419         * {@link mondrian.rolap.aggmatcher.ExplicitRules.Group} in every
420         * cubes in a given {@link RolapStar}.
421         */
422        protected List<ExplicitRules.Group> getAggGroups(RolapStar star) {
423            List<ExplicitRules.Group> aggGroups =
424                new ArrayList<ExplicitRules.Group>();
425            for (RolapCube cube : schema.getCubesWithStar(star)) {
426                if (cube.hasAggGroup() && cube.getAggGroup().hasRules()) {
427                    aggGroups.add(cube.getAggGroup());
428                }
429            }
430            return aggGroups;
431        }
432    
433        /**
434         * This method mines the RolapStar and annotes the JdbcSchema.Table
435         * dbFactTable by creating JdbcSchema.Table.Column.Usage instances. For
436         * example, a measure in the RolapStar becomes a measure usage for the
437         * column with the same name and a RolapStar foreign key column becomes a
438         * foreign key usage for the column with the same name.
439         *
440         * @param dbFactTable
441         * @param star
442         * @param msgRecorder
443         */
444        void bindToStar(final JdbcSchema.Table dbFactTable,
445                        final RolapStar star,
446                        final MessageRecorder msgRecorder) throws SQLException {
447            msgRecorder.pushContextName("AggTableManager.bindToStar");
448            try {
449                // load columns
450                dbFactTable.load();
451    
452                dbFactTable.setTableUsageType(JdbcSchema.TableUsageType.FACT);
453    
454                MondrianDef.RelationOrJoin relation = star.getFactTable().getRelation();
455                String schema = null;
456                if (relation instanceof MondrianDef.Table) {
457                    schema = ((MondrianDef.Table) relation).schema;
458                }
459                String tableName = dbFactTable.getName();
460                String alias = null;
461                dbFactTable.table = new MondrianDef.Table(schema, tableName, alias);
462    
463                for (JdbcSchema.Table.Column factColumn : dbFactTable.getColumns()) {
464                    String cname = factColumn.getName();
465                    RolapStar.Column[] rcs =
466                        star.getFactTable().lookupColumns(cname);
467    
468                    for (RolapStar.Column rc : rcs) {
469                        // its a measure
470                        if (rc instanceof RolapStar.Measure) {
471                            RolapStar.Measure rm = (RolapStar.Measure) rc;
472                            JdbcSchema.Table.Column.Usage usage =
473                                factColumn.newUsage(JdbcSchema.UsageType.MEASURE);
474                            usage.setSymbolicName(rm.getName());
475    
476                            usage.setAggregator(rm.getAggregator());
477                            usage.rMeasure = rm;
478                        }
479                    }
480    
481                    // it still might be a foreign key
482                    RolapStar.Table rTable =
483                        star.getFactTable().findTableWithLeftJoinCondition(cname);
484                    if (rTable != null) {
485                        JdbcSchema.Table.Column.Usage usage =
486                            factColumn.newUsage(JdbcSchema.UsageType.FOREIGN_KEY);
487                        usage.setSymbolicName("FOREIGN_KEY");
488                        usage.rTable = rTable;
489                    } else {
490                        RolapStar.Column rColumn =
491                            star.getFactTable().lookupColumn(cname);
492                        if ((rColumn != null) &&
493                            !(rColumn instanceof RolapStar.Measure)) {
494                            // Ok, maybe its used in a non-shared dimension
495                            // This is a column in the fact table which is
496                            // (not necessarily) a measure but is also not
497                            // a foreign key to an external dimension table.
498                            JdbcSchema.Table.Column.Usage usage =
499                                factColumn.newUsage(JdbcSchema.UsageType.FOREIGN_KEY);
500                            usage.setSymbolicName("FOREIGN_KEY");
501                            usage.rColumn = rColumn;
502                        }
503                    }
504    
505                    // warn if it has not been identified
506                    if (!factColumn.hasUsage() && getLogger().isDebugEnabled()) {
507                        String msg = mres.UnknownFactTableColumn.str(
508                            msgRecorder.getContext(),
509                            dbFactTable.getName(),
510                            factColumn.getName());
511                        getLogger().debug(msg);
512                    }
513                }
514            } finally {
515                msgRecorder.popContextName();
516            }
517        }
518    }
519    
520    // End AggTableManager.java