001    /*
002    // $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/JdbcSchema.java#23 $
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.MondrianProperties;
014    import mondrian.olap.MondrianDef;
015    import mondrian.olap.Util;
016    import mondrian.rolap.RolapAggregator;
017    import mondrian.rolap.RolapStar;
018    import mondrian.rolap.sql.SqlQuery;
019    import mondrian.resource.MondrianResource;
020    
021    import javax.sql.DataSource;
022    
023    import org.apache.log4j.Logger;
024    
025    import java.lang.ref.SoftReference;
026    import java.io.PrintWriter;
027    import java.io.StringWriter;
028    import java.sql.ResultSet;
029    import java.sql.Connection;
030    import java.sql.DatabaseMetaData;
031    import java.sql.Types;
032    import java.sql.SQLException;
033    import java.util.*;
034    
035    /**
036     * This class is used to scrape a database and store information about its
037     * tables and columnIter.
038     *
039     * <p>The structure of this information is as follows:
040     * A database has tables. A table has columnIter. A column has one or more usages.
041     * A usage might be a column being used as a foreign key or as part of a
042     * measure.
043     *
044     * <p> Tables are created when calling code requests the set of available
045     * tables. This call <code>getTables()</code> causes all tables to be loaded.
046     * But a table's columnIter are not loaded until, on a table-by-table basis,
047     * a request is made to get the set of columnIter associated with the table.
048     * Since, the AggTableManager first attempts table name matches (recognition)
049     * most tables do not match, so why load their columnIter.
050     * Of course, as a result, there are a host of methods that can throw an
051     * {@link SQLException}, rats.
052     *
053     * @author Richard M. Emberson
054     * @version $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/JdbcSchema.java#23 $
055     */
056    public class JdbcSchema {
057        private static final Logger LOGGER =
058            Logger.getLogger(JdbcSchema.class);
059    
060        private static final MondrianResource mres = MondrianResource.instance();
061    
062        /**
063         * Get the Logger.
064         */
065        public Logger getLogger() {
066            return LOGGER;
067        }
068    
069        public interface Factory {
070            JdbcSchema makeDB(DataSource dataSource);
071            void clearDB(JdbcSchema db);
072            void removeDB(JdbcSchema db);
073        }
074    
075        private static final Map<DataSource, SoftReference<JdbcSchema>> dbMap =
076            new HashMap<DataSource, SoftReference<JdbcSchema>>();
077    
078        /**
079         * How often between sweeping through the dbMap looking for nulls.
080         */
081        private static final int SWEEP_COUNT = 10;
082        private static int sweepDBCount = 0;
083    
084        public static class StdFactory implements Factory {
085            StdFactory() {
086            }
087            public JdbcSchema makeDB(DataSource dataSource) {
088                JdbcSchema db = new JdbcSchema(dataSource);
089                return db;
090            }
091            public void clearDB(JdbcSchema db) {
092                // NoOp
093            }
094            public void removeDB(JdbcSchema db) {
095                // NoOp
096            }
097        }
098    
099        private static Factory factory;
100    
101        private static void makeFactory() {
102            if (factory == null) {
103                String classname =
104                        MondrianProperties.instance().JdbcFactoryClass.get();
105                if (classname == null) {
106                    factory = new StdFactory();
107                } else {
108                    try {
109                        Class<?> clz = Class.forName(classname);
110                        factory = (Factory) clz.newInstance();
111                    } catch (ClassNotFoundException ex) {
112                        throw mres.BadJdbcFactoryClassName.ex(classname);
113                    } catch (InstantiationException ex) {
114                        throw mres.BadJdbcFactoryInstantiation.ex(classname);
115                    } catch (IllegalAccessException ex) {
116                        throw mres.BadJdbcFactoryAccess.ex(classname);
117                    }
118                }
119            }
120        }
121    
122        /**
123         * Creates or retrieves an instance of the JdbcSchema for the given
124         * DataSource.
125         *
126         * @param dataSource DataSource
127         * @return instance of the JdbcSchema for the given DataSource
128         */
129        public static synchronized JdbcSchema makeDB(DataSource dataSource) {
130            makeFactory();
131    
132            JdbcSchema db = null;
133            SoftReference<JdbcSchema> ref = dbMap.get(dataSource);
134            if (ref != null) {
135                db = ref.get();
136            }
137            if (db == null) {
138                db = factory.makeDB(dataSource);
139                dbMap.put(dataSource, new SoftReference<JdbcSchema>(db));
140            }
141    
142            sweepDB();
143    
144            return db;
145        }
146    
147        /**
148         * Clears information in a JdbcSchema associated with a DataSource.
149         *
150         * @param dataSource DataSource
151         */
152        public static synchronized void clearDB(DataSource dataSource) {
153            makeFactory();
154    
155            SoftReference<JdbcSchema> ref = dbMap.get(dataSource);
156            if (ref != null) {
157                JdbcSchema db = ref.get();
158                if (db != null) {
159                    factory.clearDB(db);
160                    db.clear();
161                } else {
162                    dbMap.remove(dataSource);
163                }
164            }
165            sweepDB();
166        }
167    
168        /**
169         * Removes a JdbcSchema associated with a DataSource.
170         *
171         * @param dataSource DataSource
172         */
173        public static synchronized void removeDB(DataSource dataSource) {
174            makeFactory();
175    
176            SoftReference<JdbcSchema> ref = dbMap.remove(dataSource);
177            if (ref != null) {
178                JdbcSchema db = ref.get();
179                if (db != null) {
180                    factory.removeDB(db);
181                    db.remove();
182                }
183            }
184            sweepDB();
185        }
186    
187        /**
188         * Every SWEEP_COUNT calls to this method, go through all elements of
189         * the dbMap removing all that either have null values (null SoftReference)
190         * or those with SoftReference with null content.
191         */
192        private static void sweepDB() {
193            if (sweepDBCount++ > SWEEP_COUNT) {
194                Iterator<SoftReference<JdbcSchema>> it = dbMap.values().iterator();
195                while (it.hasNext()) {
196                    SoftReference<JdbcSchema> ref = it.next();
197                    if ((ref == null) || (ref.get() == null)) {
198                        try {
199                            it.remove();
200                        } catch (Exception ex) {
201                            // Should not happen, but might still like to
202                            // know that something's funky.
203                            LOGGER.warn(ex);
204                        }
205                    }
206    
207                }
208                // reset
209                sweepDBCount = 0;
210            }
211        }
212    
213    
214        //
215        // Types of column usages.
216        //
217        public static final int UNKNOWN_COLUMN_USAGE         = 0x0001;
218        public static final int FOREIGN_KEY_COLUMN_USAGE     = 0x0002;
219        public static final int MEASURE_COLUMN_USAGE         = 0x0004;
220        public static final int LEVEL_COLUMN_USAGE           = 0x0008;
221        public static final int FACT_COUNT_COLUMN_USAGE      = 0x0010;
222        public static final int IGNORE_COLUMN_USAGE          = 0x0020;
223    
224        public static final String UNKNOWN_COLUMN_NAME         = "UNKNOWN";
225        public static final String FOREIGN_KEY_COLUMN_NAME     = "FOREIGN_KEY";
226        public static final String MEASURE_COLUMN_NAME         = "MEASURE";
227        public static final String LEVEL_COLUMN_NAME           = "LEVEL";
228        public static final String FACT_COUNT_COLUMN_NAME      = "FACT_COUNT";
229        public static final String IGNORE_COLUMN_NAME          = "IGNORE";
230    
231        /**
232         * Enumeration of ways that an aggregate table can use a column.
233         */
234        enum UsageType {
235            UNKNOWN,
236            FOREIGN_KEY,
237            MEASURE,
238            LEVEL,
239            FACT_COUNT,
240            IGNORE
241        }
242    
243        /**
244         * Determine if the parameter represents a single column type, i.e., the
245         * column only has one usage.
246         *
247         * @param columnType
248         * @return true if column has only one usage.
249         */
250        public static boolean isUniqueColumnType(Set<UsageType> columnType) {
251            return columnType.size() == 1;
252        }
253    
254        /**
255         * Maps from column type enum to column type name or list of names if the
256         * parameter represents more than on usage.
257         */
258        public static String convertColumnTypeToName(Set<UsageType> columnType) {
259            if (columnType.size() == 1) {
260                return columnType.iterator().next().name();
261            }
262            // it's a multi-purpose column
263            StringBuilder buf = new StringBuilder();
264            int k = 0;
265            for (UsageType usage : columnType) {
266                if (k++ > 0) {
267                    buf.append('|');
268                }
269                buf.append(usage.name());
270            }
271            return buf.toString();
272        }
273    
274        /**
275         * Converts a {@link java.sql.Types} value to a
276         * {@link mondrian.rolap.sql.SqlQuery.Datatype}.
277         *
278         * @param javaType JDBC type code, as per {@link java.sql.Types}
279         * @return Datatype
280         */
281        public static SqlQuery.Datatype getDatatype(int javaType) {
282            switch (javaType) {
283            case Types.TINYINT:
284            case Types.SMALLINT:
285            case Types.INTEGER:
286            case Types.BIGINT:
287                return SqlQuery.Datatype.Integer;
288            case Types.FLOAT:
289            case Types.REAL:
290            case Types.DOUBLE:
291            case Types.NUMERIC:
292            case Types.DECIMAL:
293                return SqlQuery.Datatype.Numeric;
294            case Types.BOOLEAN:
295                return SqlQuery.Datatype.Boolean;
296            case Types.DATE:
297                return SqlQuery.Datatype.Date;
298            case Types.TIME:
299                return SqlQuery.Datatype.Time;
300            case Types.TIMESTAMP:
301                return SqlQuery.Datatype.Timestamp;
302            case Types.CHAR:
303            case Types.VARCHAR:
304            default:
305                return SqlQuery.Datatype.String;
306            }
307        }
308    
309        /**
310         * Returns true if the parameter is a java.sql.Type text type.
311         */
312        public static boolean isText(int javaType) {
313            switch (javaType) {
314            case Types.CHAR:
315            case Types.VARCHAR:
316            case Types.LONGVARCHAR:
317                return true;
318            default:
319                return false;
320            }
321        }
322    
323        enum TableUsageType {
324            UNKNOWN,
325            FACT,
326            AGG
327        }
328    
329        /**
330         * A table in a database.
331         */
332        public class Table {
333    
334            /**
335             * A column in a table.
336             */
337            public class Column {
338    
339                /**
340                 * A usage of a column.
341                 */
342                public class Usage {
343                    private final UsageType usageType;
344                    private String symbolicName;
345                    private RolapAggregator aggregator;
346    
347                    ////////////////////////////////////////////////////
348                    //
349                    // These instance variables are used to hold
350                    // stuff which is determines at one place and
351                    // then used somewhere else. Generally, a usage
352                    // is created before all of its "stuff" can be
353                    // determined, hence, usage is not a set of classes,
354                    // rather its one class with a bunch of instance
355                    // variables which may or may not be used.
356                    //
357    
358                    // measure stuff
359                    public RolapStar.Measure rMeasure;
360    
361                    // hierarchy stuff
362                    public MondrianDef.Relation relation;
363                    public MondrianDef.Expression joinExp;
364                    public String levelColumnName;
365    
366                    // level
367                    public RolapStar.Column rColumn;
368    
369                    // for subtables
370                    public RolapStar.Table rTable;
371                    public String rightJoinConditionColumnName;
372    
373                    // It is used to hold the (possible null) prefix to
374                    // use during aggregate table generation (See AggGen).
375                    public String usagePrefix;
376                    //
377                    ////////////////////////////////////////////////////
378    
379                    Usage(UsageType usageType) {
380                        this.usageType = usageType;
381                    }
382    
383                    /**
384                     * This is the column with which this usage is associated.
385                     *
386                     * @return the usage's column.
387                     */
388                    public Column getColumn() {
389                        return JdbcSchema.Table.Column.this;
390                    }
391    
392                    /**
393                     * The column usage type.
394                     */
395                    public UsageType getUsageType() {
396                        return usageType;
397                    }
398    
399                    /**
400                     * Set the symbolic (logical) name associated with this usage.
401                     * For example, this might be the measure's name.
402                     *
403                     * @param symbolicName
404                     */
405                    public void setSymbolicName(final String symbolicName) {
406                        this.symbolicName = symbolicName;
407                    }
408    
409                    /**
410                     * Get usage's symbolic name.
411                     */
412                    public String getSymbolicName() {
413                        return symbolicName;
414                    }
415    
416                    /**
417                     * Set the aggregator associated with this usage (if its a
418                     * measure usage).
419                     *
420                     * @param aggregator
421                     */
422                    public void setAggregator(final RolapAggregator aggregator) {
423                        this.aggregator = aggregator;
424                    }
425    
426                    /**
427                     * Get the aggregator associated with this usage (if its a
428                     * measure usage, otherwise null).
429                     */
430                    public RolapAggregator getAggregator() {
431                        return aggregator;
432                    }
433    
434                    public String toString() {
435                        StringWriter sw = new StringWriter(64);
436                        PrintWriter pw = new PrintWriter(sw);
437                        print(pw, "");
438                        pw.flush();
439                        return sw.toString();
440                    }
441    
442                    public void print(final PrintWriter pw, final String prefix) {
443                        if (getSymbolicName() != null) {
444                            pw.print("symbolicName=");
445                            pw.print(getSymbolicName());
446                        }
447                        if (getAggregator() != null) {
448                            pw.print(", aggregator=");
449                            pw.print(getAggregator().getName());
450                        }
451                        pw.print(", columnType=");
452                        pw.print(getUsageType().name());
453                    }
454                }
455    
456                /** This is the name of the column. */
457                private final String name;
458    
459                /** This is the java.sql.Type enum of the column in the database. */
460                private int type;
461                /**
462                 * This is the java.sql.Type name of the column in the database.
463                 */
464                private String typeName;
465    
466                /** This is the size of the column in the database. */
467                private int columnSize;
468    
469                /** The number of fractional digits. */
470                private int decimalDigits;
471    
472                /** Radix (typically either 10 or 2). */
473                private int numPrecRadix;
474    
475                /** For char types the maximum number of bytes in the column. */
476                private int charOctetLength;
477    
478                /**
479                 * False means the column definitely does not allow NULL values.
480                 */
481                private boolean isNullable;
482    
483                public final MondrianDef.Column column;
484    
485                private final List<JdbcSchema.Table.Column.Usage> usages;
486    
487                /**
488                 * This contains the enums of all of the column's usages.
489                 */
490                private final Set<UsageType> usageTypes =
491                    Util.enumSetNoneOf(UsageType.class);
492    
493                private Column(final String name) {
494                    this.name = name;
495                    this.column =
496                        new MondrianDef.Column(
497                            JdbcSchema.Table.this.getName(),
498                            name);
499                    this.usages = new ArrayList<JdbcSchema.Table.Column.Usage>();
500                }
501    
502                /**
503                 * For testing ONLY
504                JdbcSchema.Table.Column copy() {
505                    Column column = new Column(name);
506                    column.type = type;
507                    column.typeName = typeName;
508                    column.columnSize = columnSize;
509                    column.decimalDigits = decimalDigits;
510                    column.numPrecRadix = numPrecRadix;
511                    column.charOctetLength = charOctetLength;
512                    column.isNullable = isNullable;
513    
514                    return column;
515                }
516                 */
517                /**
518                 * For testing ONLY
519                void clearUsages() {
520                    // empty
521                }
522                 */
523    
524    
525                /**
526                 * This is the column's name in the database, not a symbolic name.
527                 */
528                public String getName() {
529                    return name;
530                }
531    
532                /**
533                 * Set the columnIter java.sql.Type enun of the column.
534                 *
535                 * @param type
536                 */
537                private void setType(final int type) {
538                    this.type = type;
539                }
540    
541                /**
542                 * Get the columnIter java.sql.Type enun of the column.
543                 */
544                public int getType() {
545                    return type;
546                }
547    
548                /**
549                 * Set the columnIter java.sql.Type name.
550                 *
551                 * @param typeName
552                 */
553                private void setTypeName(final String typeName) {
554                    this.typeName = typeName;
555                }
556    
557                /**
558                 * Get the columnIter java.sql.Type name.
559                 */
560                public String getTypeName() {
561                    return typeName;
562                }
563    
564                /**
565                 * Get this column's table.
566                 */
567                public Table getTable() {
568                    return JdbcSchema.Table.this;
569                }
570    
571                /**
572                 * Return true if this column is numeric.
573                 */
574                public SqlQuery.Datatype getDatatype() {
575                    return JdbcSchema.getDatatype(getType());
576                }
577    
578                /**
579                 * Set the size in bytes of the column in the database.
580                 *
581                 * @param columnSize
582                 */
583                private void setColumnSize(final int columnSize) {
584                    this.columnSize = columnSize;
585                }
586    
587                /**
588                 * Get the size in bytes of the column in the database.
589                 *
590                 */
591                public int getColumnSize() {
592                    return columnSize;
593                }
594    
595                /**
596                 * Set number of fractional digits.
597                 *
598                 * @param decimalDigits
599                 */
600                private void setDecimalDigits(final int decimalDigits) {
601                    this.decimalDigits = decimalDigits;
602                }
603    
604                /**
605                 * Get number of fractional digits.
606                 */
607                public int getDecimalDigits() {
608                    return decimalDigits;
609                }
610    
611                /**
612                 * Set Radix (typically either 10 or 2).
613                 *
614                 * @param numPrecRadix
615                 */
616                private void setNumPrecRadix(final int numPrecRadix) {
617                    this.numPrecRadix = numPrecRadix;
618                }
619    
620                /**
621                 * Get Radix (typically either 10 or 2).
622                 */
623                public int getNumPrecRadix() {
624                    return numPrecRadix;
625                }
626    
627                /**
628                 * For char types the maximum number of bytes in the column.
629                 *
630                 * @param charOctetLength
631                 */
632                private void setCharOctetLength(final int charOctetLength) {
633                    this.charOctetLength = charOctetLength;
634                }
635    
636                /**
637                 * For char types the maximum number of bytes in the column.
638                 */
639                public int getCharOctetLength() {
640                    return charOctetLength;
641                }
642    
643                /**
644                 * False means the column definitely does not allow NULL values.
645                 *
646                 * @param isNullable
647                 */
648                private void setIsNullable(final boolean isNullable) {
649                    this.isNullable = isNullable;
650                }
651    
652                /**
653                 * False means the column definitely does not allow NULL values.
654                 */
655                public boolean isNullable() {
656                    return isNullable;
657                }
658    
659                /**
660                 * How many usages does this column have. A column has
661                 * between 0 and N usages. It has no usages if usages is some
662                 * administrative column. It has one usage if, for example, its
663                 * the fact_count column or a level column (for a collapsed
664                 * dimension aggregate). It might have 2 usages if its a foreign key
665                 * that is also used as a measure. If its a column used in N
666                 * measures, then usages will have N usages.
667                 */
668                public int numberOfUsages() {
669                    return usages.size();
670                }
671    
672                /**
673                 * Return true if the column has at least one usage.
674                 */
675                public boolean hasUsage() {
676                    return (usages.size() != 0);
677                }
678    
679                /**
680                 * Return true if the column has at least one usage of the given
681                 * column type.
682                 */
683                public boolean hasUsage(UsageType columnType) {
684                    return usageTypes.contains(columnType);
685                }
686    
687                /**
688                 * Get an iterator over all usages.
689                 */
690                public List<Usage> getUsages() {
691                    return usages;
692                }
693    
694                /**
695                 * Get an iterator over all usages of the given column type.
696                 */
697                public Iterator<Usage> getUsages(UsageType usageType) {
698    
699                    // Yes, this is legal.
700                    class ColumnTypeIterator implements Iterator<Usage> {
701                        private final Iterator<Usage> usageIter;
702                        private final UsageType usageType;
703                        private Usage nextUsage;
704    
705                        ColumnTypeIterator(
706                            final List<Usage> usages,
707                            final UsageType columnType)
708                        {
709                            this.usageIter = usages.iterator();
710                            this.usageType = columnType;
711                        }
712    
713                        public boolean hasNext() {
714                            while (usageIter.hasNext()) {
715                                Usage usage = usageIter.next();
716                                if (usage.getUsageType() == this.usageType) {
717                                    nextUsage = usage;
718                                    return true;
719                                }
720    
721                            }
722                            nextUsage = null;
723                            return false;
724                        }
725    
726                        public Usage next() {
727                            return nextUsage;
728                        }
729    
730                        public void remove() {
731                            usageIter.remove();
732                        }
733                    }
734    
735                    return new ColumnTypeIterator(getUsages(), usageType);
736                }
737    
738                /**
739                 * Create a new usage of a given column type.
740                 */
741                public Usage newUsage(UsageType usageType) {
742                    this.usageTypes.add(usageType);
743    
744                    Usage usage = new Usage(usageType);
745                    usages.add(usage);
746                    return usage;
747                }
748    
749                public String toString() {
750                    StringWriter sw = new StringWriter(256);
751                    PrintWriter pw = new PrintWriter(sw);
752                    print(pw, "");
753                    pw.flush();
754                    return sw.toString();
755                }
756    
757                public void print(final PrintWriter pw, final String prefix) {
758                    pw.print(prefix);
759                    pw.print("name=");
760                    pw.print(getName());
761                    pw.print(", typename=");
762                    pw.print(getTypeName());
763                    pw.print(", size=");
764                    pw.print(getColumnSize());
765    
766                    switch (getType()) {
767                    case Types.TINYINT:
768                    case Types.SMALLINT:
769                    case Types.INTEGER:
770                    case Types.BIGINT:
771                    case Types.FLOAT:
772                    case Types.REAL:
773                    case Types.DOUBLE:
774                        break;
775                    case Types.NUMERIC:
776                    case Types.DECIMAL:
777                        pw.print(", decimalDigits=");
778                        pw.print(getDecimalDigits());
779                        pw.print(", numPrecRadix=");
780                        pw.print(getNumPrecRadix());
781                        break;
782                    case Types.CHAR:
783                    case Types.VARCHAR:
784                        pw.print(", charOctetLength=");
785                        pw.print(getCharOctetLength());
786                        break;
787                    case Types.LONGVARCHAR:
788                    case Types.DATE:
789                    case Types.TIME:
790                    case Types.TIMESTAMP:
791                    case Types.BINARY:
792                    case Types.VARBINARY:
793                    case Types.LONGVARBINARY:
794                    default:
795                        break;
796                    }
797                    pw.print(", isNullable=");
798                    pw.print(isNullable());
799    
800                    if (hasUsage()) {
801                        pw.print(" Usages [");
802                        for (Usage usage : getUsages()) {
803                            pw.print('(');
804                            usage.print(pw, prefix);
805                            pw.print(')');
806                        }
807                        pw.println("]");
808                    }
809                }
810            }
811    
812            /** Name of table. */
813            private final String name;
814            /** Map from column name to column. */
815            private Map<String, Column> columnMap;
816            /** Sum of all of the table's column's column sizes. */
817            private int totalColumnSize;
818            /**
819             * Is the table a fact, aggregate or other table type.
820             * Note: this assumes that a table has only ONE usage.
821             */
822            private TableUsageType tableUsageType;
823    
824            /**
825             * Typical table types are: "TABLE", "VIEW", "SYSTEM TABLE",
826             * "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM".
827             * (Depends what comes out of JDBC.)
828             */
829            private final String tableType;
830    
831            // mondriandef stuff
832            public MondrianDef.Table table;
833    
834            private boolean allColumnsLoaded;
835    
836            private Table(final String name, String tableType) {
837                this.name = name;
838                this.tableUsageType = TableUsageType.UNKNOWN;
839                this.tableType = tableType;
840            }
841    
842            public void load() throws SQLException {
843                loadColumns();
844            }
845    
846            /**
847             * For testing ONLY
848            JdbcSchema.Table copy() {
849                Table table = new Table(name);
850                table.totalColumnSize = totalColumnSize;
851                table.tableUsage = tableUsage;
852                table.tableType = tableType;
853    
854                Map m = table.getColumnMap();
855                for (Iterator usageIter = getColumns(); usageIter.hasNext();) {
856                    Column column = (Column) usageIter.next();
857                    m.put(column.getName(), column.copy());
858                }
859    
860                return table;
861            }
862             */
863            /**
864             * For testing ONLY
865            void clearUsages() {
866                this.tableUsage = UNKNOWN_TABLE_USAGE;
867                for (Iterator usageIter = getColumns(); usageIter.hasNext();) {
868                    Column column = (Column) usageIter.next();
869                    column.clearUsages();
870                }
871            }
872             */
873    
874            /**
875             * Get the name of the table.
876             */
877            public String getName() {
878                return name;
879            }
880    
881            /**
882             * Get the total size of a row (sum of the column sizes).
883             */
884            public int getTotalColumnSize() {
885                return totalColumnSize;
886            }
887    
888            /**
889             * Get the number of rows in the table.
890             */
891            public int getNumberOfRows() {
892                return -1;
893            }
894    
895            /**
896             * Returns the collection of columns in this Table.
897             */
898            public Collection<Column> getColumns() {
899                return getColumnMap().values();
900            }
901    
902            /**
903             * Returns an iterator over all column usages of a given type.
904             */
905            public Iterator<JdbcSchema.Table.Column.Usage> getColumnUsages(
906                final UsageType usageType)
907            {
908    
909                class CTIterator implements Iterator<JdbcSchema.Table.Column.Usage> {
910                    private final Iterator<Column> columnIter;
911                    private final UsageType columnType;
912                    private Iterator<JdbcSchema.Table.Column.Usage> usageIter;
913                    private JdbcSchema.Table.Column.Usage nextObject;
914    
915                    CTIterator(Collection<Column> columns, UsageType columnType) {
916                        this.columnIter = columns.iterator();
917                        this.columnType = columnType;
918                    }
919    
920                    public boolean hasNext() {
921                        while (true) {
922                            while ((usageIter == null) || ! usageIter.hasNext()) {
923                                if (! columnIter.hasNext()) {
924                                    nextObject = null;
925                                    return false;
926                                }
927                                Column c = columnIter.next();
928                                usageIter = c.getUsages().iterator();
929                            }
930                            JdbcSchema.Table.Column.Usage usage = usageIter.next();
931                            if (usage.getUsageType() == columnType) {
932                                nextObject = usage;
933                                return true;
934                            }
935                        }
936                    }
937                    public JdbcSchema.Table.Column.Usage next() {
938                        return nextObject;
939                    }
940                    public void remove() {
941                        usageIter.remove();
942                    }
943                }
944                return new CTIterator(getColumns(), usageType);
945            }
946    
947            /**
948             * Get a column by its name.
949             */
950            public Column getColumn(final String columnName) {
951                return getColumnMap().get(columnName);
952            }
953    
954            /**
955             * Return true if this table contains a column with the given name.
956             */
957            public boolean constainsColumn(final String columnName) {
958                return getColumnMap().containsKey(columnName);
959            }
960    
961            /**
962             * Set the table usage (fact, aggregate or other).
963             *
964             * @param tableUsageType
965             */
966            public void setTableUsageType(final TableUsageType tableUsageType) {
967                // if usageIter has already been set, then usageIter can NOT be reset
968                if ((this.tableUsageType != TableUsageType.UNKNOWN) &&
969                        (this.tableUsageType != tableUsageType)) {
970    
971                    throw mres.AttemptToChangeTableUsage.ex(
972                        getName(),
973                        this.tableUsageType.name(),
974                        tableUsageType.name());
975                }
976                this.tableUsageType = tableUsageType;
977            }
978    
979            /**
980             * Get the table's usage type.
981             */
982            public TableUsageType getTableUsageType() {
983                return tableUsageType;
984            }
985    
986            /**
987             * Get the table's type.
988             */
989            public String getTableType() {
990                return tableType;
991            }
992    
993            public String toString() {
994                StringWriter sw = new StringWriter(256);
995                PrintWriter pw = new PrintWriter(sw);
996                print(pw, "");
997                pw.flush();
998                return sw.toString();
999            }
1000            public void print(final PrintWriter pw, final String prefix) {
1001                pw.print(prefix);
1002                pw.println("Table:");
1003                String subprefix = prefix + "  ";
1004                String subsubprefix = subprefix + "  ";
1005    
1006                pw.print(subprefix);
1007                pw.print("name=");
1008                pw.print(getName());
1009                pw.print(", type=");
1010                pw.print(getTableType());
1011                pw.print(", usage=");
1012                pw.println(getTableUsageType().name());
1013    
1014                pw.print(subprefix);
1015                pw.print("totalColumnSize=");
1016                pw.println(getTotalColumnSize());
1017    
1018                pw.print(subprefix);
1019                pw.println("Columns: [");
1020                for (Column column : getColumnMap().values()) {
1021                    column.print(pw, subsubprefix);
1022                    pw.println();
1023                }
1024                pw.print(subprefix);
1025                pw.println("]");
1026            }
1027    
1028            /**
1029             * Get all of the columnIter associated with a table and create Column
1030             * objects with the column's name, type, type name and column size.
1031             *
1032             * @throws SQLException
1033             */
1034            private void loadColumns() throws SQLException {
1035                if (! allColumnsLoaded) {
1036                    Connection conn = getDataSource().getConnection();
1037                    try {
1038                        DatabaseMetaData dmd = conn.getMetaData();
1039    
1040                        String schema = JdbcSchema.this.getSchemaName();
1041                        String catalog = JdbcSchema.this.getCatalogName();
1042                        String tableName = getName();
1043                        String columnNamePattern = "%";
1044    
1045                        ResultSet rs = null;
1046                        try {
1047                            Map<String, Column> map = getColumnMap();
1048                            rs = dmd.getColumns(catalog,
1049                                                schema,
1050                                                tableName,
1051                                                columnNamePattern);
1052                            while (rs.next()) {
1053                                String name = rs.getString(4);
1054                                int type = rs.getInt(5);
1055                                String typeName = rs.getString(6);
1056                                int columnSize = rs.getInt(7);
1057                                int decimalDigits = rs.getInt(9);
1058                                int numPrecRadix = rs.getInt(10);
1059                                int charOctetLength = rs.getInt(16);
1060                                String isNullable = rs.getString(18);
1061    
1062                                Column column = new Column(name);
1063                                column.setType(type);
1064                                column.setTypeName(typeName);
1065                                column.setColumnSize(columnSize);
1066                                column.setDecimalDigits(decimalDigits);
1067                                column.setNumPrecRadix(numPrecRadix);
1068                                column.setCharOctetLength(charOctetLength);
1069                                column.setIsNullable(!"NO".equals(isNullable));
1070    
1071                                map.put(name, column);
1072                                totalColumnSize += column.getColumnSize();
1073                            }
1074                        } finally {
1075                            if (rs != null) {
1076                                rs.close();
1077                            }
1078                        }
1079                    } finally {
1080                        try {
1081                            conn.close();
1082                        } catch (SQLException e) {
1083                            //ignore
1084                        }
1085                    }
1086    
1087                    allColumnsLoaded = true;
1088                }
1089            }
1090    
1091            private Map<String, Column> getColumnMap() {
1092                if (columnMap == null) {
1093                    columnMap = new HashMap<String, Column>();
1094                }
1095                return columnMap;
1096            }
1097        }
1098    
1099        private DataSource dataSource;
1100        private String schema;
1101        private String catalog;
1102        private boolean allTablesLoaded;
1103    
1104        /**
1105         * Tables by name. We use a sorted map so {@link #getTables()}'s output
1106         * is in deterministic order.
1107         */
1108        private final SortedMap<String, Table> tables = new TreeMap<String, Table>();
1109    
1110        JdbcSchema(final DataSource dataSource) {
1111            this.dataSource = dataSource;
1112        }
1113    
1114        /**
1115         * This forces the tables to be loaded.
1116         *
1117         * @throws SQLException
1118         */
1119        public void load() throws SQLException {
1120            loadTables();
1121        }
1122    
1123        /**
1124         * For testing ONLY
1125        JdbcSchema copy() {
1126            JdbcSchema jdbcSchema = new JdbcSchema(dataSource);
1127            jdbcSchema.setSchemaName(getSchemaName());
1128            jdbcSchema.setCatalogName(getCatalogName());
1129    
1130            Map m = jdbcSchema.getTablesMap();
1131            for (Iterator usageIter = getTables(); usageIter.hasNext();) {
1132                Table table = (Table) usageIter.next();
1133                m.put(table.getName(), table.copy());
1134            }
1135    
1136            return jdbcSchema;
1137        }
1138         */
1139    
1140        /**
1141         * For testing ONLY
1142        void clearUsages() {
1143            for (Iterator usageIter = getTables(); usageIter.hasNext();) {
1144                Table table = (Table) usageIter.next();
1145                table.clearUsages();
1146            }
1147        }
1148         */
1149    
1150        protected void clear() {
1151            // keep the DataSource, clear/reset everything else
1152            allTablesLoaded = false;
1153            schema = null;
1154            catalog = null;
1155            tables.clear();
1156        }
1157        protected void remove() {
1158            // set ALL instance variables to null
1159            clear();
1160            dataSource = null;
1161        }
1162    
1163        /**
1164         * This is used for testing allowing one to load tables and their columnIter
1165         * from more than one datasource
1166         */
1167        void resetAllTablesLoaded() {
1168            allTablesLoaded = false;
1169        }
1170    
1171        public DataSource getDataSource() {
1172            return dataSource;
1173        }
1174    
1175        protected void setDataSource(DataSource dataSource) {
1176            this.dataSource = dataSource;
1177        }
1178    
1179        /**
1180         * Set the database's schema name.
1181         *
1182         * @param schema
1183         */
1184        public void setSchemaName(final String schema) {
1185            this.schema = schema;
1186        }
1187    
1188        /**
1189         * Get the database's schema name.
1190         */
1191        public String getSchemaName() {
1192            return schema;
1193        }
1194    
1195        /**
1196         * Set the database's catalog name.
1197         */
1198        public void setCatalogName(final String catalog) {
1199            this.catalog = catalog;
1200        }
1201    
1202        /**
1203         * Get the database's catalog name.
1204         */
1205        public String getCatalogName() {
1206            return catalog;
1207        }
1208    
1209        /**
1210         * Returns the database's tables. The collection is sorted by table name.
1211         */
1212        public synchronized Collection<Table> getTables() {
1213            return getTablesMap().values();
1214        }
1215    
1216        /**
1217         * Gets a table by name.
1218         */
1219        public synchronized Table getTable(final String tableName) {
1220            return getTablesMap().get(tableName);
1221        }
1222    
1223        public String toString() {
1224            StringWriter sw = new StringWriter(256);
1225            PrintWriter pw = new PrintWriter(sw);
1226            print(pw, "");
1227            pw.flush();
1228            return sw.toString();
1229        }
1230        public void print(final PrintWriter pw, final String prefix) {
1231            pw.print(prefix);
1232            pw.println("JdbcSchema:");
1233            String subprefix = prefix + "  ";
1234            String subsubprefix = subprefix + "  ";
1235    
1236            pw.print(subprefix);
1237            pw.println("Tables: [");
1238            Iterator it = getTablesMap().values().iterator();
1239            while (it.hasNext()) {
1240                Table table = (Table) it.next();
1241                table.print(pw, subsubprefix);
1242            }
1243            pw.print(subprefix);
1244            pw.println("]");
1245        }
1246    
1247        /**
1248         * This method gets all of the tables (and views) in the database.
1249         * If called a second time, this method is a no-op.
1250         *
1251         * @throws SQLException
1252         */
1253        private void loadTables() throws SQLException {
1254            if (! allTablesLoaded) {
1255                Connection conn = getDataSource().getConnection();
1256                DatabaseMetaData dmd = conn.getMetaData();
1257    
1258                String schema = getSchemaName();
1259                String catalog = getCatalogName();
1260                String[] tableTypes = { "TABLE", "VIEW" };
1261                String tableName = "%";
1262    
1263                ResultSet rs = null;
1264                try {
1265                    rs = dmd.getTables(catalog,
1266                                       schema,
1267                                       tableName,
1268                                       tableTypes);
1269                    if (rs != null) {
1270                        while (rs.next()) {
1271                            addTable(rs);
1272                        }
1273                    } else {
1274                        getLogger().debug("ERROR: rs == null");
1275                    }
1276                } finally {
1277                    if (rs != null) {
1278                        rs.close();
1279                    }
1280                }
1281                try {
1282                    conn.close();
1283                } catch (SQLException e) {
1284                    //ignore
1285                }
1286    
1287                allTablesLoaded = true;
1288            }
1289        }
1290    
1291        /**
1292         * Make a Table from an ResultSet - the table's name is the ResultSet third
1293         * entry.
1294         *
1295         * @param rs
1296         * @throws SQLException
1297         */
1298        protected void addTable(final ResultSet rs) throws SQLException {
1299            String name = rs.getString(3);
1300            String tableType = rs.getString(4);
1301            Table table = new Table(name, tableType);
1302    
1303            tables.put(table.getName(), table);
1304        }
1305    
1306        private SortedMap<String, Table> getTablesMap() {
1307            return tables;
1308        }
1309    
1310        public static synchronized void clearAllDBs() {
1311            factory = null;
1312            makeFactory();
1313        }
1314    }
1315    
1316    // End JdbcSchema.java