View Javadoc
1   /*
2    * This file is a part of the SchemaSpy project (http://schemaspy.sourceforge.net).
3    * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 John Currier
4    *
5    * SchemaSpy is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU Lesser General Public
7    * License as published by the Free Software Foundation; either
8    * version 2.1 of the License, or (at your option) any later version.
9    *
10   * SchemaSpy is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with this library; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18   */
19  package net.sourceforge.schemaspy.model;
20  
21  import java.sql.Connection;
22  import java.sql.DatabaseMetaData;
23  import java.sql.PreparedStatement;
24  import java.sql.ResultSet;
25  import java.sql.SQLException;
26  import java.text.SimpleDateFormat;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Date;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.MissingResourceException;
37  import java.util.Properties;
38  import java.util.Set;
39  import java.util.StringTokenizer;
40  import java.util.logging.Level;
41  import java.util.logging.Logger;
42  import java.util.regex.Matcher;
43  import java.util.regex.Pattern;
44  import net.sourceforge.schemaspy.Config;
45  import net.sourceforge.schemaspy.model.xml.SchemaMeta;
46  import net.sourceforge.schemaspy.model.xml.TableMeta;
47  import net.sourceforge.schemaspy.util.CaseInsensitiveMap;
48  
49  public class Database {
50      private final String databaseName;
51      private final String schema;
52      private String description;
53      private final Map<String, Table> tables = new CaseInsensitiveMap<Table>();
54      private final Map<String, View> views = new CaseInsensitiveMap<View>();
55      private final Map<String, Table> remoteTables = new CaseInsensitiveMap<Table>(); // key: schema.tableName value: RemoteTable
56      private final DatabaseMetaData meta;
57      private final Connection connection;
58      private final String connectTime = new SimpleDateFormat("EEE MMM dd HH:mm z yyyy").format(new Date());
59      private Set<String> sqlKeywords;
60      private Pattern invalidIdentifierPattern;
61      private final Logger logger = Logger.getLogger(getClass().getName());
62      private final boolean fineEnabled = logger.isLoggable(Level.FINE);
63  
64      public Database(Config config, Connection connection, DatabaseMetaData meta, String name, String schema, Properties properties, SchemaMeta schemaMeta) throws SQLException, MissingResourceException {
65          this.connection = connection;
66          this.meta = meta;
67          databaseName = name;
68          this.schema = schema;
69          description = config.getDescription();
70  
71          initTables(meta, properties, config);
72          if (config.isViewsEnabled())
73              initViews(meta, properties, config);
74  
75          initCheckConstraints(properties);
76          initTableIds(properties);
77          initIndexIds(properties);
78          initTableComments(properties);
79          initTableColumnComments(properties);
80          initViewComments(properties);
81          initViewColumnComments(properties);
82  
83          connectTables();
84          updateFromXmlMetadata(schemaMeta);
85      }
86  
87      public String getName() {
88          return databaseName;
89      }
90  
91      public String getSchema() {
92          return schema;
93      }
94  
95      /**
96       * Details of the database type that's running under the covers.
97       *
98       * @return null if a description wasn't specified.
99       */
100     public String getDescription() {
101         return description;
102     }
103 
104     public Collection<Table> getTables() {
105         return tables.values();
106     }
107 
108     /**
109      * Return a {@link Map} of all {@link Table}s keyed by their name.
110      *
111      * @return
112      */
113     public Map<String, Table> getTablesByName() {
114     	return tables;
115     }
116 
117     public Collection<View> getViews() {
118         return views.values();
119     }
120 
121     public Collection<Table> getRemoteTables() {
122         return remoteTables.values();
123     }
124 
125     public Connection getConnection() {
126         return connection;
127     }
128 
129     public DatabaseMetaData getMetaData() {
130         return meta;
131     }
132 
133     public String getConnectTime() {
134         return connectTime;
135     }
136 
137     public String getDatabaseProduct() {
138         try {
139             return meta.getDatabaseProductName() + " - " + meta.getDatabaseProductVersion();
140         } catch (SQLException exc) {
141             return "";
142         }
143     }
144 
145     /**
146      *  "macro" to validate that a table is somewhat valid
147      */
148     class NameValidator {
149         private final String clazz;
150         private final Pattern include;
151         private final Pattern exclude;
152         private final Set<String> validTypes;
153 
154         /**
155          * @param clazz table or view
156          * @param include
157          * @param exclude
158          * @param validTypes
159          */
160         NameValidator(String clazz, Pattern include, Pattern exclude, String[] validTypes) {
161             this.clazz = clazz;
162             this.include = include;
163             this.exclude = exclude;
164             this.validTypes = new HashSet<String>();
165             for (String type : validTypes)
166             {
167                 this.validTypes.add(type.toUpperCase());
168             }
169         }
170 
171         /**
172          * Returns <code>true</code> if the table/view name is deemed "valid"
173          *
174          * @param name name of the table or view
175          * @param type type as returned by metadata.getTables():TABLE_TYPE
176          * @return
177          */
178         boolean isValid(String name, String type) {
179             // some databases (MySQL) return more than we wanted
180             if (!validTypes.contains(type.toUpperCase()))
181                 return false;
182 
183             // Oracle 10g introduced problematic flashback tables
184             // with bizarre illegal names
185             // Naming Convention "BIN$"${globalUID}${version}
186             // http://docs.oracle.com/cd/B19306_01/backup.102/b14192/flashptr004.htm#i1016977
187             if (name.indexOf("BIN$") == 0) {
188                 if (fineEnabled) {
189                     logger.fine("Excluding " + clazz + " " + name +
190                                 ": \"BIN$\" prefix implies  a (deleted) table in the Oracle Recycle Bin ");
191                 }
192                 return false;
193             }
194 
195             if (exclude.matcher(name).matches()) {
196                 if (fineEnabled) {
197                     logger.fine("Excluding " + clazz + " " + name +
198                                 ": matches exclusion pattern \"" + exclude + '"');
199                 }
200                 return false;
201             }
202 
203             boolean valid = include.matcher(name).matches();
204             if (fineEnabled) {
205                 if (valid) {
206                     logger.fine("Including " + clazz + " " + name +
207                                 ": matches inclusion pattern \"" + include + '"');
208                 } else {
209                     logger.fine("Excluding " + clazz + " " + name +
210                                 ": doesn't match inclusion pattern \"" + include + '"');
211                 }
212             }
213             return valid;
214         }
215     }
216 
217     /**
218      * Create/initialize any tables in the schema.
219 
220      * @param metadata
221      * @param properties
222      * @param config
223      * @throws SQLException
224      */
225     private void initTables(final DatabaseMetaData metadata, final Properties properties,
226                             final Config config) throws SQLException {
227         final Pattern include = config.getTableInclusions();
228         final Pattern exclude = config.getTableExclusions();
229         final int maxThreads = config.getMaxDbThreads();
230 
231         String[] types = getTypes("tableTypes", "TABLE", properties);
232         NameValidator validator = new NameValidator("table", include, exclude, types);
233         List<BasicTableMeta> entries = getBasicTableMeta(metadata, true, properties, types);
234 
235         TableCreator creator;
236         if (maxThreads == 1) {
237             creator = new TableCreator();
238         } else {
239             // creating tables takes a LONG time (based on JProbe analysis),
240             // so attempt to speed it up by doing several in parallel.
241             // note that it's actually DatabaseMetaData.getIndexInfo() that's expensive
242 
243             creator = new ThreadedTableCreator(maxThreads);
244 
245             // "prime the pump" so if there's a database problem we'll probably see it now
246             // and not in a secondary thread
247             while (!entries.isEmpty()) {
248                 BasicTableMeta entry = entries.remove(0);
249 
250                 if (validator.isValid(entry.name, entry.type)) {
251                     new TableCreator().create(entry, properties);
252                     break;
253                 }
254             }
255         }
256 
257         // kick off the secondary threads to do the creation in parallel
258         for (BasicTableMeta entry : entries) {
259             if (validator.isValid(entry.name, entry.type)) {
260                 creator.create(entry, properties);
261             }
262         }
263 
264         // wait for everyone to finish
265         creator.join();
266     }
267 
268     /**
269      * Create/initialize any views in the schema.
270      *
271      * @param metadata
272      * @param properties
273      * @param config
274      * @throws SQLException
275      */
276     private void initViews(DatabaseMetaData metadata, Properties properties,
277                             Config config) throws SQLException {
278         Pattern includeTables = config.getTableInclusions();
279         Pattern excludeTables = config.getTableExclusions();
280         Pattern excludeColumns = config.getColumnExclusions();
281         Pattern excludeIndirectColumns = config.getIndirectColumnExclusions();
282 
283         String[] types = getTypes("viewTypes", "VIEW", properties);
284         NameValidator validator = new NameValidator("view", includeTables, excludeTables, types);
285 
286         for (BasicTableMeta entry : getBasicTableMeta(metadata, false, properties, types)) {
287             if (validator.isValid(entry.name, entry.type)) {
288                 View view = new View(this, entry.schema, entry.name, entry.remarks,
289                                     entry.viewSql, properties,
290                                     excludeIndirectColumns, excludeColumns);
291                 views.put(view.getName(), view);
292                 if (logger.isLoggable(Level.FINE)) {
293                     logger.fine("Found details of view " + view.getName());
294                 } else {
295                     System.out.print('.');
296                 }
297             }
298         }
299     }
300 
301     /**
302      * Collection of fundamental table/view metadata
303      */
304     private class BasicTableMeta
305     {
306         @SuppressWarnings("hiding")
307         final String schema;
308         final String name;
309         final String type;
310         final String remarks;
311         final String viewSql;
312         final long numRows;  // -1 if not determined
313 
314         /**
315          * @param schema
316          * @param name
317          * @param type typically "TABLE" or "VIEW"
318          * @param remarks
319          * @param text optional textual SQL used to create the view
320          * @param numRows number of rows, or -1 if not determined
321          */
322         BasicTableMeta(String schema, String name, String type, String remarks, String text, long numRows)
323         {
324             this.schema = schema;
325             this.name = name;
326             this.type = type;
327             this.remarks = remarks;
328             viewSql = text;
329             this.numRows = numRows;
330         }
331     }
332 
333     /**
334      * Return a list of basic details of the tables in the schema.
335      *
336      * @param metadata
337      * @param forTables true if we're getting table data, false if getting view data
338      * @param properties
339      * @return
340      * @throws SQLException
341      */
342     private List<BasicTableMeta> getBasicTableMeta(DatabaseMetaData metadata,
343                                                     boolean forTables,
344                                                     Properties properties,
345                                                     String... types) throws SQLException {
346         String queryName = forTables ? "selectTablesSql" : "selectViewsSql";
347         String sql = properties.getProperty(queryName);
348         List<BasicTableMeta> basics = new ArrayList<BasicTableMeta>();
349         ResultSet rs = null;
350 
351         if (sql != null) {
352             String clazz = forTables ? "table" : "view";
353             PreparedStatement stmt = null;
354 
355             try {
356                 stmt = prepareStatement(sql, null);
357                 rs = stmt.executeQuery();
358 
359                 while (rs.next()) {
360                     String name = rs.getString(clazz + "_name");
361                     String sch = getOptionalString(rs, clazz + "_schema");
362                     if (sch == null)
363                         sch = schema;
364                     String remarks = getOptionalString(rs, clazz + "_comment");
365                     String text = forTables ? null : getOptionalString(rs, "view_definition");
366                     String rows = forTables ? getOptionalString(rs, "table_rows") : null;
367                     long numRows = rows == null ? -1 : Long.parseLong(rows);
368 
369                     basics.add(new BasicTableMeta(sch, name, clazz, remarks, text, numRows));
370                 }
371             } catch (SQLException sqlException) {
372                 // don't die just because this failed
373                 System.out.flush();
374                 System.err.println();
375                 System.err.println("Failed to retrieve " + clazz + " names with custom SQL: " + sqlException);
376                 System.err.println(sql);
377             } finally {
378                 if (rs != null)
379                     rs.close();
380                 if (stmt != null)
381                     stmt.close();
382             }
383         }
384 
385         if (basics.isEmpty()) {
386             rs = metadata.getTables(null, schema, "%", types);
387 
388             try {
389                 while (rs.next()) {
390                     String name = rs.getString("TABLE_NAME");
391                     String type = rs.getString("TABLE_TYPE");
392                     String schem = rs.getString("TABLE_SCHEM");
393                     String remarks = getOptionalString(rs, "REMARKS");
394 
395                     basics.add(new BasicTableMeta(schem, name, type, remarks, null, -1));
396                 }
397             } catch (SQLException exc) {
398                 if (forTables)
399                     throw exc;
400 
401                 System.out.flush();
402                 System.err.println();
403                 System.err.println("Ignoring view " + rs.getString("TABLE_NAME") + " due to exception:");
404                 exc.printStackTrace();
405                 System.err.println("Continuing analysis.");
406             } finally {
407                 if (rs != null)
408                     rs.close();
409             }
410         }
411 
412         return basics;
413     }
414 
415     /**
416      * Return a database-specific array of types from the .properties file
417      * with the specified property name.
418      *
419      * @param propName
420      * @param defaultValue
421      * @param props
422      * @return
423      */
424     private String[] getTypes(String propName, String defaultValue, Properties props) {
425         String value = props.getProperty(propName, defaultValue);
426         List<String> types = new ArrayList<String>();
427         for (String type : value.split(",")) {
428             type = type.trim();
429             if (type.length() > 0)
430                 types.add(type);
431         }
432 
433         return types.toArray(new String[types.size()]);
434     }
435 
436     /**
437      * Some databases don't play nice with their metadata.
438      * E.g. Oracle doesn't have a REMARKS column at all.
439      * This method ignores those types of failures, replacing them with null.
440      */
441     public String getOptionalString(ResultSet rs, String columnName)
442     {
443         try {
444             return rs.getString(columnName);
445         } catch (SQLException ignore) {
446             return null;
447         }
448     }
449 
450     private void initCheckConstraints(Properties properties) throws SQLException {
451         String sql = properties.getProperty("selectCheckConstraintsSql");
452         if (sql != null) {
453             PreparedStatement stmt = null;
454             ResultSet rs = null;
455 
456             try {
457                 stmt = prepareStatement(sql, null);
458                 rs = stmt.executeQuery();
459 
460                 while (rs.next()) {
461                     String tableName = rs.getString("table_name");
462                     Table table = tables.get(tableName);
463                     if (table != null)
464                         table.addCheckConstraint(rs.getString("constraint_name"), rs.getString("text"));
465                 }
466             } catch (SQLException sqlException) {
467                 // don't die just because this failed
468                 System.err.println();
469                 System.err.println("Failed to retrieve check constraints: " + sqlException);
470                 System.err.println(sql);
471             } finally {
472                 if (rs != null)
473                     rs.close();
474                 if (stmt != null)
475                     stmt.close();
476             }
477         }
478     }
479 
480     private void initTableIds(Properties properties) throws SQLException {
481         String sql = properties.getProperty("selectTableIdsSql");
482         if (sql != null) {
483             PreparedStatement stmt = null;
484             ResultSet rs = null;
485 
486             try {
487                 stmt = prepareStatement(sql, null);
488                 rs = stmt.executeQuery();
489 
490                 while (rs.next()) {
491                     String tableName = rs.getString("table_name");
492                     Table table = tables.get(tableName);
493                     if (table != null)
494                         table.setId(rs.getObject("table_id"));
495                 }
496             } catch (SQLException sqlException) {
497                 System.err.println();
498                 System.err.println(sql);
499                 throw sqlException;
500             } finally {
501                 if (rs != null)
502                     rs.close();
503                 if (stmt != null)
504                     stmt.close();
505             }
506         }
507     }
508 
509     private void initIndexIds(Properties properties) throws SQLException {
510         String sql = properties.getProperty("selectIndexIdsSql");
511         if (sql != null) {
512             PreparedStatement stmt = null;
513             ResultSet rs = null;
514 
515             try {
516                 stmt = prepareStatement(sql, null);
517                 rs = stmt.executeQuery();
518 
519                 while (rs.next()) {
520                     String tableName = rs.getString("table_name");
521                     Table table = tables.get(tableName);
522                     if (table != null) {
523                         TableIndex index = table.getIndex(rs.getString("index_name"));
524                         if (index != null)
525                             index.setId(rs.getObject("index_id"));
526                     }
527                 }
528             } catch (SQLException sqlException) {
529                 System.err.println();
530                 System.err.println(sql);
531                 throw sqlException;
532             } finally {
533                 if (rs != null)
534                     rs.close();
535                 if (stmt != null)
536                     stmt.close();
537             }
538         }
539     }
540 
541     /**
542      * Initializes table comments.
543      * If the SQL also returns view comments then they're plugged into the
544      * appropriate views.
545      *
546      * @param properties
547      * @throws SQLException
548      */
549     private void initTableComments(Properties properties) throws SQLException {
550         String sql = properties.getProperty("selectTableCommentsSql");
551         if (sql != null) {
552             PreparedStatement stmt = null;
553             ResultSet rs = null;
554 
555             try {
556                 stmt = prepareStatement(sql, null);
557                 rs = stmt.executeQuery();
558 
559                 while (rs.next()) {
560                     String tableName = rs.getString("table_name");
561                     Table table = tables.get(tableName);
562                     if (table == null)
563                         table = views.get(tableName);
564 
565                     if (table != null)
566                         table.setComments(rs.getString("comments"));
567                 }
568             } catch (SQLException sqlException) {
569                 // don't die just because this failed
570                 System.err.println();
571                 System.err.println("Failed to retrieve table/view comments: " + sqlException);
572                 System.err.println(sql);
573             } finally {
574                 if (rs != null)
575                     rs.close();
576                 if (stmt != null)
577                     stmt.close();
578             }
579         }
580     }
581 
582     /**
583      * Initializes view comments.
584      *
585      * @param properties
586      * @throws SQLException
587      */
588     private void initViewComments(Properties properties) throws SQLException {
589         String sql = properties.getProperty("selectViewCommentsSql");
590         if (sql != null) {
591             PreparedStatement stmt = null;
592             ResultSet rs = null;
593 
594             try {
595                 stmt = prepareStatement(sql, null);
596                 rs = stmt.executeQuery();
597 
598                 while (rs.next()) {
599                     String viewName = rs.getString("view_name");
600                     if (viewName == null)
601                         viewName = rs.getString("table_name");
602                     Table view = views.get(viewName);
603 
604                     if (view != null)
605                         view.setComments(rs.getString("comments"));
606                 }
607             } catch (SQLException sqlException) {
608                 // don't die just because this failed
609                 System.err.println();
610                 System.err.println("Failed to retrieve table/view comments: " + sqlException);
611                 System.err.println(sql);
612             } finally {
613                 if (rs != null)
614                     rs.close();
615                 if (stmt != null)
616                     stmt.close();
617             }
618         }
619     }
620 
621     /**
622      * Initializes table column comments.
623      * If the SQL also returns view column comments then they're plugged into the
624      * appropriate views.
625      *
626      * @param properties
627      * @throws SQLException
628      */
629     private void initTableColumnComments(Properties properties) throws SQLException {
630         String sql = properties.getProperty("selectColumnCommentsSql");
631         if (sql != null) {
632             PreparedStatement stmt = null;
633             ResultSet rs = null;
634 
635             try {
636                 stmt = prepareStatement(sql, null);
637                 rs = stmt.executeQuery();
638 
639                 while (rs.next()) {
640                     String tableName = rs.getString("table_name");
641                     Table table = tables.get(tableName);
642                     if (table == null)
643                         table = views.get(tableName);
644 
645                     if (table != null) {
646                         TableColumn column = table.getColumn(rs.getString("column_name"));
647                         if (column != null)
648                             column.setComments(rs.getString("comments"));
649                     }
650                 }
651             } catch (SQLException sqlException) {
652                 // don't die just because this failed
653                 System.err.println();
654                 System.err.println("Failed to retrieve column comments: " + sqlException);
655                 System.err.println(sql);
656             } finally {
657                 if (rs != null)
658                     rs.close();
659                 if (stmt != null)
660                     stmt.close();
661             }
662         }
663     }
664 
665     /**
666      * Initializes view column comments.
667      *
668      * @param properties
669      * @throws SQLException
670      */
671     private void initViewColumnComments(Properties properties) throws SQLException {
672         String sql = properties.getProperty("selectViewColumnCommentsSql");
673         if (sql != null) {
674             PreparedStatement stmt = null;
675             ResultSet rs = null;
676 
677             try {
678                 stmt = prepareStatement(sql, null);
679                 rs = stmt.executeQuery();
680 
681                 while (rs.next()) {
682                     String viewName = rs.getString("view_name");
683                     if (viewName == null)
684                         viewName = rs.getString("table_name");
685                     Table view = views.get(viewName);
686 
687                     if (view != null) {
688                         TableColumn column = view.getColumn(rs.getString("column_name"));
689                         if (column != null)
690                             column.setComments(rs.getString("comments"));
691                     }
692                 }
693             } catch (SQLException sqlException) {
694                 // don't die just because this failed
695                 System.err.println();
696                 System.err.println("Failed to retrieve view column comments: " + sqlException);
697                 System.err.println(sql);
698             } finally {
699                 if (rs != null)
700                     rs.close();
701                 if (stmt != null)
702                     stmt.close();
703             }
704         }
705     }
706 
707     /**
708      * Create a <code>PreparedStatement</code> from the specified SQL.
709      * The SQL can contain these named parameters (but <b>not</b> question marks).
710      * <ol>
711      * <li>:schema - replaced with the name of the schema
712      * <li>:owner - alias for :schema
713      * <li>:table - replaced with the name of the table
714      * </ol>
715      * @param sql String - SQL without question marks
716      * @param tableName String - <code>null</code> if the statement doesn't deal with <code>Table</code>-level details.
717      * @throws SQLException
718      * @return PreparedStatement
719      */
720     public PreparedStatement prepareStatement(String sql, String tableName) throws SQLException {
721         StringBuilder sqlBuf = new StringBuilder(sql);
722         List<String> sqlParams = getSqlParams(sqlBuf, tableName); // modifies sqlBuf
723         PreparedStatement stmt = getConnection().prepareStatement(sqlBuf.toString());
724 
725         try {
726             for (int i = 0; i < sqlParams.size(); ++i) {
727                 stmt.setString(i + 1, sqlParams.get(i).toString());
728             }
729         } catch (SQLException exc) {
730             stmt.close();
731             throw exc;
732         }
733 
734         return stmt;
735     }
736 
737     public Table addRemoteTable(String remoteSchema, String remoteTableName, String baseSchema, Properties properties, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
738         String fullName = remoteSchema + "." + remoteTableName;
739         Table remoteTable = remoteTables.get(fullName);
740         if (remoteTable == null) {
741             if (properties != null)
742                 remoteTable = new RemoteTable(this, remoteSchema, remoteTableName, baseSchema, properties, excludeIndirectColumns, excludeColumns);
743             else
744                 remoteTable = new ExplicitRemoteTable(this, remoteSchema, remoteTableName, baseSchema);
745 
746             logger.fine("Adding remote table " + fullName);
747             remoteTable.connectForeignKeys(tables, excludeIndirectColumns, excludeColumns);
748             remoteTables.put(fullName, remoteTable);
749         }
750 
751         return remoteTable;
752     }
753 
754     /**
755      * Return an uppercased <code>Set</code> of all SQL keywords used by a database
756      *
757      * @return
758      * @throws SQLException
759      */
760     public Set<String> getSqlKeywords() throws SQLException {
761         if (sqlKeywords == null) {
762             // from http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt:
763             String[] sql92Keywords =
764                 ("ADA" +
765                 "| C | CATALOG_NAME | CHARACTER_SET_CATALOG | CHARACTER_SET_NAME" +
766                 "| CHARACTER_SET_SCHEMA | CLASS_ORIGIN | COBOL | COLLATION_CATALOG" +
767                 "| COLLATION_NAME | COLLATION_SCHEMA | COLUMN_NAME | COMMAND_FUNCTION | COMMITTED" +
768                 "| CONDITION_NUMBER | CONNECTION_NAME | CONSTRAINT_CATALOG | CONSTRAINT_NAME" +
769                 "| CONSTRAINT_SCHEMA | CURSOR_NAME" +
770                 "| DATA | DATETIME_INTERVAL_CODE | DATETIME_INTERVAL_PRECISION | DYNAMIC_FUNCTION" +
771                 "| FORTRAN" +
772                 "| LENGTH" +
773                 "| MESSAGE_LENGTH | MESSAGE_OCTET_LENGTH | MESSAGE_TEXT | MORE | MUMPS" +
774                 "| NAME | NULLABLE | NUMBER" +
775                 "| PASCAL | PLI" +
776                 "| REPEATABLE | RETURNED_LENGTH | RETURNED_OCTET_LENGTH | RETURNED_SQLSTATE" +
777                 "| ROW_COUNT" +
778                 "| SCALE | SCHEMA_NAME | SERIALIZABLE | SERVER_NAME | SUBCLASS_ORIGIN" +
779                 "| TABLE_NAME | TYPE" +
780                 "| UNCOMMITTED | UNNAMED" +
781                 "| ABSOLUTE | ACTION | ADD | ALL | ALLOCATE | ALTER | AND" +
782                 "| ANY | ARE | AS | ASC" +
783                 "| ASSERTION | AT | AUTHORIZATION | AVG" +
784                 "| BEGIN | BETWEEN | BIT | BIT_LENGTH | BOTH | BY" +
785                 "| CASCADE | CASCADED | CASE | CAST | CATALOG | CHAR | CHARACTER | CHAR_LENGTH" +
786                 "| CHARACTER_LENGTH | CHECK | CLOSE | COALESCE | COLLATE | COLLATION" +
787                 "| COLUMN | COMMIT | CONNECT | CONNECTION | CONSTRAINT" +
788                 "| CONSTRAINTS | CONTINUE" +
789                 "| CONVERT | CORRESPONDING | COUNT | CREATE | CROSS | CURRENT" +
790                 "| CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | CURRENT_USER | CURSOR" +
791                 "| DATE | DAY | DEALLOCATE | DEC | DECIMAL | DECLARE | DEFAULT | DEFERRABLE" +
792                 "| DEFERRED | DELETE | DESC | DESCRIBE | DESCRIPTOR | DIAGNOSTICS" +
793                 "| DISCONNECT | DISTINCT | DOMAIN | DOUBLE | DROP" +
794                 "| ELSE | END | END-EXEC | ESCAPE | EXCEPT | EXCEPTION" +
795                 "| EXEC | EXECUTE | EXISTS" +
796                 "| EXTERNAL | EXTRACT" +
797                 "| FALSE | FETCH | FIRST | FLOAT | FOR | FOREIGN | FOUND | FROM | FULL" +
798                 "| GET | GLOBAL | GO | GOTO | GRANT | GROUP" +
799                 "| HAVING | HOUR" +
800                 "| IDENTITY | IMMEDIATE | IN | INDICATOR | INITIALLY | INNER | INPUT" +
801                 "| INSENSITIVE | INSERT | INT | INTEGER | INTERSECT | INTERVAL | INTO | IS" +
802                 "| ISOLATION" +
803                 "| JOIN" +
804                 "| KEY" +
805                 "| LANGUAGE | LAST | LEADING | LEFT | LEVEL | LIKE | LOCAL | LOWER" +
806                 "| MATCH | MAX | MIN | MINUTE | MODULE | MONTH" +
807                 "| NAMES | NATIONAL | NATURAL | NCHAR | NEXT | NO | NOT | NULL" +
808                 "| NULLIF | NUMERIC" +
809                 "| OCTET_LENGTH | OF | ON | ONLY | OPEN | OPTION | OR" +
810                 "| ORDER | OUTER" +
811                 "| OUTPUT | OVERLAPS" +
812                 "| PAD | PARTIAL | POSITION | PRECISION | PREPARE | PRESERVE | PRIMARY" +
813                 "| PRIOR | PRIVILEGES | PROCEDURE | PUBLIC" +
814                 "| READ | REAL | REFERENCES | RELATIVE | RESTRICT | REVOKE | RIGHT" +
815                 "| ROLLBACK | ROWS" +
816                 "| SCHEMA | SCROLL | SECOND | SECTION | SELECT | SESSION | SESSION_USER | SET" +
817                 "| SIZE | SMALLINT | SOME | SPACE | SQL | SQLCODE | SQLERROR | SQLSTATE" +
818                 "| SUBSTRING | SUM | SYSTEM_USER" +
819                 "| TABLE | TEMPORARY | THEN | TIME | TIMESTAMP | TIMEZONE_HOUR | TIMEZONE_MINUTE" +
820                 "| TO | TRAILING | TRANSACTION | TRANSLATE | TRANSLATION | TRIM | TRUE" +
821                 "| UNION | UNIQUE | UNKNOWN | UPDATE | UPPER | USAGE | USER | USING" +
822                 "| VALUE | VALUES | VARCHAR | VARYING | VIEW" +
823                 "| WHEN | WHENEVER | WHERE | WITH | WORK | WRITE" +
824                 "| YEAR" +
825                 "| ZONE").split("|,\\s*");
826 
827             String[] nonSql92Keywords = getMetaData().getSQLKeywords().toUpperCase().split(",\\s*");
828 
829             sqlKeywords = new HashSet<String>();
830             sqlKeywords.addAll(Arrays.asList(sql92Keywords));
831             sqlKeywords.addAll(Arrays.asList(nonSql92Keywords));
832         }
833 
834         return sqlKeywords;
835     }
836 
837     /**
838      * Return <code>id</code> quoted if required, otherwise return <code>id</code>
839      *
840      * @param id
841      * @return
842      * @throws SQLException
843      */
844     public String getQuotedIdentifier(String id) throws SQLException {
845         // look for any character that isn't valid (then matcher.find() returns true)
846         Matcher matcher = getInvalidIdentifierPattern().matcher(id);
847 
848         boolean quotesRequired = matcher.find() || getSqlKeywords().contains(id.toUpperCase());
849 
850         if (quotesRequired) {
851             // name contains something that must be quoted
852             String quote = getMetaData().getIdentifierQuoteString().trim();
853             return quote + id + quote;
854         }
855 
856         // no quoting necessary
857         return id;
858     }
859 
860     /**
861      * Return a <code>Pattern</code> whose matcher will return <code>true</code>
862      * when run against an identifier that contains a character that is not
863      * acceptable by the database without being quoted.
864      */
865     private Pattern getInvalidIdentifierPattern() throws SQLException {
866         if (invalidIdentifierPattern == null) {
867             String validChars = "a-zA-Z0-9_";
868             String reservedRegexChars = "-&^";
869             String extraValidChars = getMetaData().getExtraNameCharacters();
870             for (int i = 0; i < extraValidChars.length(); ++i) {
871                 char ch = extraValidChars.charAt(i);
872                 if (reservedRegexChars.indexOf(ch) >= 0)
873                     validChars += "\\";
874                 validChars += ch;
875             }
876 
877             invalidIdentifierPattern = Pattern.compile("[^" + validChars + "]");
878         }
879 
880         return invalidIdentifierPattern;
881     }
882 
883     /**
884      * Replaces named parameters in <code>sql</code> with question marks and
885      * returns appropriate matching values in the returned <code>List</code> of <code>String</code>s.
886      *
887      * @param sql StringBuffer input SQL with named parameters, output named params are replaced with ?'s.
888      * @param tableName String
889      * @return List of Strings
890      *
891      * @see #prepareStatement(String, String)
892      */
893     private List<String> getSqlParams(StringBuilder sql, String tableName) {
894         Map<String, String> namedParams = new HashMap<String, String>();
895         @SuppressWarnings("hiding")
896         String schema = getSchema();
897         if (schema == null)
898             schema = getName(); // some 'schema-less' db's treat the db name like a schema (unusual case)
899         namedParams.put(":schema", schema);
900         namedParams.put(":owner", schema); // alias for :schema
901         if (tableName != null) {
902             namedParams.put(":table", tableName);
903             namedParams.put(":view", tableName); // alias for :table
904         }
905 
906         List<String> sqlParams = new ArrayList<String>();
907         int nextColon = sql.indexOf(":");
908         while (nextColon != -1) {
909             String paramName = new StringTokenizer(sql.substring(nextColon), " ,\"')").nextToken();
910             String paramValue = namedParams.get(paramName);
911             if (paramValue == null)
912                 throw new InvalidConfigurationException("Unexpected named parameter '" + paramName + "' found in SQL '" + sql + "'");
913             sqlParams.add(paramValue);
914             sql.replace(nextColon, nextColon + paramName.length(), "?"); // replace with a ?
915             nextColon = sql.indexOf(":", nextColon);
916         }
917 
918         return sqlParams;
919     }
920 
921     /**
922      * Take the supplied XML-based metadata and update our model of the schema with it
923      *
924      * @param schemaMeta
925      * @throws SQLException
926      */
927     private void updateFromXmlMetadata(SchemaMeta schemaMeta) throws SQLException {
928         if (schemaMeta != null) {
929             final Pattern excludeNone = Pattern.compile("[^.]");
930             final Properties noProps = new Properties();
931 
932             description = schemaMeta.getComments();
933 
934             // done in three passes:
935             // 1: create any new tables
936             // 2: add/mod columns
937             // 3: connect
938 
939             // add the newly defined tables and columns first
940             for (TableMeta tableMeta : schemaMeta.getTables()) {
941                 Table table;
942 
943                 if (tableMeta.getRemoteSchema() != null) {
944                     table = remoteTables.get(tableMeta.getRemoteSchema() + '.' + tableMeta.getName());
945                     if (table == null) {
946                         table = addRemoteTable(tableMeta.getRemoteSchema(), tableMeta.getName(), getSchema(), null, excludeNone, excludeNone);
947                     }
948                 } else {
949                     table = tables.get(tableMeta.getName());
950 
951                     if (table == null)
952                         table = views.get(tableMeta.getName());
953 
954                     if (table == null) {
955                         table = new Table(Database.this, getSchema(), tableMeta.getName(), null, noProps, excludeNone, excludeNone);
956                         tables.put(table.getName(), table);
957                     }
958                 }
959 
960                 table.update(tableMeta);
961             }
962 
963             // then tie the tables together
964             for (TableMeta tableMeta : schemaMeta.getTables()) {
965                 Table table;
966 
967                 if (tableMeta.getRemoteSchema() != null) {
968                     table = remoteTables.get(tableMeta.getRemoteSchema() + '.' + tableMeta.getName());
969                 } else {
970                     table = tables.get(tableMeta.getName());
971                     if (table == null)
972                         table = views.get(tableMeta.getName());
973                 }
974 
975                 table.connect(tableMeta, tables, remoteTables);
976             }
977         }
978     }
979 
980     private void connectTables() throws SQLException {
981         Pattern excludeColumns = Config.getInstance().getColumnExclusions();
982         Pattern excludeIndirectColumns = Config.getInstance().getIndirectColumnExclusions();
983 
984         for (Table table : tables.values()) {
985             table.connectForeignKeys(tables, excludeIndirectColumns, excludeColumns);
986         }
987     }
988 
989     /**
990      * Single-threaded implementation of a class that creates tables
991      */
992     private class TableCreator {
993         private final Pattern excludeColumns = Config.getInstance().getColumnExclusions();
994         private final Pattern excludeIndirectColumns = Config.getInstance().getIndirectColumnExclusions();
995 
996         /**
997          * Create a table and put it into <code>tables</code>
998          */
999         void create(BasicTableMeta tableMeta, Properties properties) throws SQLException {
1000             createImpl(tableMeta, properties);
1001         }
1002 
1003         protected void createImpl(BasicTableMeta tableMeta, Properties properties) throws SQLException {
1004             Table table = new Table(Database.this, tableMeta.schema, tableMeta.name, tableMeta.remarks, properties, excludeIndirectColumns, excludeColumns);
1005             if (tableMeta.numRows != -1) {
1006                 table.setNumRows(tableMeta.numRows);
1007             }
1008 
1009             synchronized (tables) {
1010                 tables.put(table.getName(), table);
1011             }
1012 
1013             if (logger.isLoggable(Level.FINE)) {
1014                 logger.fine("Found details of table " + table.getName());
1015             } else {
1016                 System.out.print('.');
1017             }
1018         }
1019 
1020         /**
1021          * Wait for all of the tables to be created.
1022          * By default this does nothing since this implementation isn't threaded.
1023          */
1024         void join() {
1025         }
1026     }
1027 
1028     /**
1029      * Multi-threaded implementation of a class that creates tables
1030      */
1031     private class ThreadedTableCreator extends TableCreator {
1032         private final Set<Thread> threads = new HashSet<Thread>();
1033         private final int maxThreads;
1034 
1035         ThreadedTableCreator(int maxThreads) {
1036             this.maxThreads = maxThreads;
1037         }
1038 
1039         @Override
1040         void create(final BasicTableMeta tableMeta, final Properties properties) throws SQLException {
1041             Thread runner = new Thread() {
1042                 @Override
1043                 public void run() {
1044                     try {
1045                         createImpl(tableMeta, properties);
1046                     } catch (SQLException exc) {
1047                         exc.printStackTrace(); // nobody above us in call stack...dump it here
1048                     } finally {
1049                         synchronized (threads) {
1050                             threads.remove(this);
1051                             threads.notify();
1052                         }
1053                     }
1054                 }
1055             };
1056 
1057             synchronized (threads) {
1058                 // wait for enough 'room'
1059                 while (threads.size() >= maxThreads) {
1060                     try {
1061                         threads.wait();
1062                     } catch (InterruptedException interrupted) {
1063                     }
1064                 }
1065 
1066                 threads.add(runner);
1067             }
1068 
1069             runner.start();
1070         }
1071 
1072         /**
1073          * Wait for all of the started threads to complete
1074          */
1075         @Override
1076         public void join() {
1077             while (true) {
1078                 Thread thread;
1079 
1080                 synchronized (threads) {
1081                     Iterator<Thread> iter = threads.iterator();
1082                     if (!iter.hasNext())
1083                         break;
1084 
1085                     thread = iter.next();
1086                 }
1087 
1088                 try {
1089                     thread.join();
1090                 } catch (InterruptedException exc) {
1091                 }
1092             }
1093         }
1094     }
1095 }