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