View Javadoc

1   package com.workingdogs.village;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.PrintWriter;
24  import java.sql.Connection;
25  import java.sql.DatabaseMetaData;
26  import java.sql.PreparedStatement;
27  import java.sql.ResultSet;
28  import java.sql.ResultSetMetaData;
29  import java.sql.SQLException;
30  import java.util.ArrayList;
31  import java.util.Enumeration;
32  import java.util.Hashtable;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.TreeMap;
37  
38  /***
39   * The Schema object represents the <a href="Column.html">Columns</a> in a database table. It contains a collection of <a
40   * href="Column.html">Column</a> objects.
41   *
42   * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
43   * @author John D. McNally
44   * @version $Revision: 568 $
45   */
46  public final class Schema
47  {
48      /*** TODO: DOCUMENT ME! */
49      private String tableName;
50  
51      /*** TODO: DOCUMENT ME! */
52      private String columnsAttribute;
53  
54      /*** TODO: DOCUMENT ME! */
55      private int numberOfColumns;
56  
57      /*** TODO: DOCUMENT ME! */
58      private Column [] columns;
59      private Map columnNumberByName;
60  
61      /*** TODO: DOCUMENT ME! */
62      private static final Hashtable schemaCache = new Hashtable();
63  
64      /***
65       * This attribute is used to complement columns in the event that this schema represents more than one table.  Its keys are
66       * String contains table names and its elements are Hashtables containing columns.
67       */
68      private Hashtable tableHash = null;
69  
70      /*** TODO: DOCUMENT ME! */
71      private boolean singleTable = true;
72  
73      /***
74       * A blank Schema object
75       */
76      public Schema()
77      {
78          this.tableName = "";
79          this.columnsAttribute = null;
80          this.numberOfColumns = 0;
81      }
82  
83      /***
84       * Initialize all table schemas reachable from this connection
85       *
86       * @param conn a database connection
87       * @throws SQLException if retrieving the database meta data is unsuccessful
88       */
89      public static void initSchemas(Connection conn) throws SQLException
90      {
91          ResultSet allCol = null;
92  
93          try
94          {
95              DatabaseMetaData databaseMetaData  = conn.getMetaData();
96              String connURL = databaseMetaData.getURL();
97              allCol = databaseMetaData.getColumns(
98                      conn.getCatalog(), null, null, null);
99  
100             while (true)
101             {
102                 Schema schema = new Schema();
103 
104                 schema.setAttributes("*");
105                 schema.singleTable = true;
106                 schema.populate(allCol);
107 
108                 if (schema.numberOfColumns > 0)
109                 {
110                     String keyValue = connURL + schema.tableName;
111 
112                     synchronized (schemaCache)
113                     {
114                         schemaCache.put(keyValue, schema);
115                     }
116                 }
117                 else
118                 {
119                     break;
120                 }
121             }
122         }
123         finally
124         {
125             if (allCol != null)
126             {
127                 try
128                 {
129                     allCol.close();
130                 }
131                 catch (SQLException e)
132                 {
133                     //Do nothing
134                 }
135             }
136         }
137     }
138 
139     /***
140      * Creates a Schema with all columns
141      *
142      * @param conn
143      * @param tableName
144      *
145      * @return an instance of myself
146      *
147      * @exception SQLException
148      * @exception DataSetException
149      */
150     public Schema schema(Connection conn, String tableName)
151             throws SQLException, DataSetException
152     {
153         return schema(conn, tableName, "*");
154     }
155 
156     /***
157      * Creates a Schema with the named columns in the columnsAttribute
158      *
159      * @param conn
160      * @param tableName
161      * @param columnsAttribute
162      *
163      * @return an instance of myself
164      *
165      * @exception SQLException
166      * @exception DataSetException
167      */
168     public Schema schema(Connection conn, String tableName, String columnsAttribute)
169             throws SQLException, DataSetException
170     {
171         if (columnsAttribute == null)
172         {
173             columnsAttribute = "*";
174         }
175 
176         PreparedStatement stmt = null;
177 
178         try
179         {
180             Schema tableSchema = null;
181             String keyValue = conn.getMetaData().getURL() + tableName;
182 
183             synchronized (schemaCache)
184             {
185                 tableSchema = (Schema) schemaCache.get(keyValue);
186 
187                 if (tableSchema == null)
188                 {
189                     String sql = "SELECT " + columnsAttribute + " FROM " + tableName + " WHERE 1 = -1";
190 
191                     stmt = conn.prepareStatement(sql);
192 
193                     if (stmt != null)
194                     {
195                         stmt.executeQuery();
196                         tableSchema = this;
197                         tableSchema.setTableName(tableName);
198                         tableSchema.setAttributes(columnsAttribute);
199                         tableSchema.populate(stmt.getMetaData(), tableName, null);
200                         schemaCache.put(keyValue, tableSchema);
201                     }
202                     else
203                     {
204                         throw new DataSetException("Couldn't retrieve schema for " + tableName);
205                     }
206                 }
207             }
208 
209             return tableSchema;
210         }
211         finally
212         {
213             if (stmt != null)
214             {
215                 try
216                 {
217                     stmt.close();
218                 }
219                 catch (SQLException e)
220                 {
221                     //Do nothing
222                 }
223             }
224         }
225     }
226 
227     /***
228      * Appends data to the tableName that this schema was first created with.
229      *
230      * <P></p>
231      *
232      * @param app String to append to tableName
233      *
234      * @see TableDataSet#tableQualifier(java.lang.String)
235      */
236     void appendTableName(String app)
237     {
238         this.tableName = this.tableName + " " + app;
239     }
240 
241     /***
242      * List of columns to select from the table
243      *
244      * @return the list of columns to select from the table
245      */
246     public String attributes()
247     {
248         return this.columnsAttribute;
249     }
250 
251     /***
252      * Returns the requested Column object at index i
253      *
254      * @param i
255      *
256      * @return the requested column
257      *
258      * @exception DataSetException
259      */
260     public Column column(int i)
261             throws DataSetException
262     {
263         if (i == 0)
264         {
265             throw new DataSetException("Columns are 1 based");
266         }
267         else if (i > numberOfColumns)
268         {
269             throw new DataSetException("There are only " + numberOfColumns() + " available!");
270         }
271 
272         try
273         {
274             return columns[i];
275         }
276         catch (Exception e)
277         {
278             throw new DataSetException("Column number: " + numberOfColumns() + " does not exist!");
279         }
280     }
281 
282     /***
283      * Returns the requested Column object by name
284      *
285      * @param colName
286      *
287      * @return the requested column
288      *
289      * @exception DataSetException
290      */
291     public Column column(String colName)
292             throws DataSetException
293     {
294         return column(index(colName));
295     }
296 
297     /***
298      * Returns the requested Column object by name
299      *
300      * @param colName
301      *
302      * @return the requested column
303      *
304      * @exception DataSetException
305      */
306     public Column getColumn(String colName)
307             throws DataSetException
308     {
309         int dot = colName.indexOf('.');
310 
311         if (dot > 0)
312         {
313             String table = colName.substring(0, dot);
314             String col = colName.substring(dot + 1);
315 
316             return getColumn(table, col);
317         }
318 
319         return column(index(colName));
320     }
321 
322     /***
323      * Returns the requested Column object belonging to the specified table by name
324      *
325      * @param tableName
326      * @param colName
327      *
328      * @return the requested column, null if a column by the specified name does not exist.
329      *
330      * @exception DataSetException
331      */
332     public Column getColumn(String tableName, String colName)
333             throws DataSetException
334     {
335         return (Column) ((Hashtable) tableHash.get(tableName)).get(colName);
336     }
337 
338     /***
339      * Returns an array of columns
340      *
341      * @return an array of columns
342      */
343     Column [] getColumns()
344     {
345         return this.columns;
346     }
347 
348     /***
349      * returns the table name that this Schema represents
350      *
351      * @return the table name that this Schema represents
352      *
353      * @throws DataSetException TODO: DOCUMENT ME!
354      */
355     public String getTableName()
356             throws DataSetException
357     {
358         if (singleTable)
359         {
360             return tableName;
361         }
362         else
363         {
364             throw new DataSetException("This schema represents several tables.");
365         }
366     }
367 
368     /***
369      * returns all table names that this Schema represents
370      *
371      * @return the table names that this Schema represents
372      */
373     public String [] getAllTableNames()
374     {
375         Enumeration e = tableHash.keys();
376         String [] tableNames = new String[tableHash.size()];
377 
378         for (int i = 0; e.hasMoreElements(); i++)
379         {
380             tableNames[i] = (String) e.nextElement();
381         }
382 
383         return tableNames;
384     }
385 
386     /***
387      * Gets the index position of a named column.  If multiple tables are represented and they have columns with the same name,
388      * this method returns the first one listed, if the table name is not specified.
389      *
390      * @param colName
391      *
392      * @return the requested column index integer
393      *
394      * @exception DataSetException
395      */
396     public int index(String colName)
397             throws DataSetException
398     {
399         Integer position = (Integer) columnNumberByName.get(colName);
400 
401         if (position != null)
402         {
403             return position.intValue();
404         }
405         else
406         {
407             throw new DataSetException("Column name: " + colName + " does not exist!");
408         }
409     }
410 
411     /***
412      * Gets the index position of a named column.
413      *
414      * @param tableName
415      * @param colName
416      *
417      * @return the requested column index integer
418      *
419      * @exception DataSetException
420      */
421     public int index(String tableName, String colName)
422             throws DataSetException
423     {
424         return index(tableName + "." + colName);
425     }
426 
427     /***
428      * Checks to see if this DataSet represents one table in the database.
429      *
430      * @return true if only one table is represented, false otherwise.
431      */
432     public boolean isSingleTable()
433     {
434         return singleTable;
435     }
436 
437     /***
438      * Gets the number of columns in this Schema
439      *
440      * @return integer number of columns
441      */
442     public int numberOfColumns()
443     {
444         return this.numberOfColumns;
445     }
446 
447     /***
448      * Internal method which populates this Schema object with Columns.
449      *
450      * @param meta The meta data of the ResultSet used to build this Schema.
451      * @param tableName The name of the table referenced in this schema, or null if unknown or multiple tables are involved.
452      * @param conn The connection whose URL serves as a cache key prefix
453      *
454      * @exception SQLException
455      * @exception DataSetException
456      */
457     void populate(ResultSetMetaData meta, String tableName, Connection conn)
458             throws SQLException, DataSetException
459     {
460         this.numberOfColumns = meta.getColumnCount();
461         columns = new Column[numberOfColumns() + 1];
462         columnNumberByName = new TreeMap(String.CASE_INSENSITIVE_ORDER);
463 
464         String connURL = (conn != null) ? conn.getMetaData().getURL() : null;
465 
466         for (int i = 1; i <= numberOfColumns(); i++)
467         {
468             String metaColumnName = meta.getColumnName(i);
469             String metaTableName = null;
470 
471             // Workaround for Sybase jConnect 5.2 and older.
472             try
473             {
474                 metaTableName = meta.getTableName(i);
475 
476                 // ResultSetMetaData may report table name as the empty
477                 // string when a database-specific function has been
478                 // called to generate a Column.
479                 if ((metaTableName == null) || metaTableName.equals(""))
480                 {
481                     if (tableName != null)
482                     {
483                         metaTableName = tableName;
484                     }
485                     else
486                     {
487                         metaTableName = "";
488                     }
489                 }
490             }
491             catch (RuntimeException e)
492             {
493                 if (tableName != null)
494                 {
495                     metaTableName = tableName;
496                 }
497                 else
498                 {
499                     metaTableName = "";
500                 }
501             }
502 
503             Column col = null;
504 
505             if (metaTableName.length() > 0 && connURL != null)
506             {
507                 Schema tableSchema = null; // schema(conn, metaTableName);
508 
509                 synchronized (schemaCache)
510                 {
511                     tableSchema = (Schema) schemaCache.get(connURL + metaTableName);
512                 }
513 
514                 if (tableSchema != null)
515                 {
516                     try
517                     {
518                         col = tableSchema.column(metaColumnName);
519                     }
520                     catch (DataSetException e)
521                     {
522                         // column does not exist, ignore
523                     }
524                 }
525             }
526 
527             // Not found in cache
528             if (col == null)
529             {
530                 col = new Column();
531                 col.populate(meta, i, metaTableName, metaColumnName);
532             }
533 
534             columns[i] = col;
535             Integer position = new Integer(i);
536             columnNumberByName.put(metaColumnName, position);
537             columnNumberByName.put(metaTableName + "." + metaColumnName, position);
538 
539             if ((i > 1) && !col.getTableName().equalsIgnoreCase(columns[i - 1].getTableName()))
540             {
541                 singleTable = false;
542             }
543         }
544 
545         // Avoid creating a Hashtable in the most common case where only one
546         // table is involved, even though this makes the multiple table case
547         // more expensive because the table/column info is duplicated.
548         if (singleTable)
549         {
550             // If available, use a the caller supplied table name.
551             if ((tableName != null) && (tableName.length() > 0))
552             {
553                 setTableName(tableName);
554             }
555             else
556             {
557                 // Since there's only one table involved, attempt to set the
558                 // table name to that of the first column.  Sybase jConnect
559                 // 5.2 and older will fail, in which case we are screwed.
560                 try
561                 {
562                     setTableName(columns[1].getTableName());
563                 }
564                 catch (Exception e)
565                 {
566                     setTableName("");
567                 }
568             }
569         }
570         else
571         {
572             tableHash = new Hashtable((int) ((1.25 * numberOfColumns) + 1));
573 
574             for (int i = 1; i <= numberOfColumns(); i++)
575             {
576                 Hashtable columnHash;
577 
578                 if (tableHash.containsKey(columns[i].getTableName()))
579                 {
580                     columnHash = (Hashtable) tableHash.get(columns[i].getTableName());
581                 }
582                 else
583                 {
584                     columnHash = new Hashtable((int) ((1.25 * numberOfColumns) + 1));
585                     tableHash.put(columns[i].getTableName(), columnHash);
586                 }
587 
588                 columnHash.put(columns[i].name(), columns[i]);
589             }
590         }
591     }
592 
593     /***
594      * Internal method which populates this Schema object with Columns.
595      *
596      * @param meta The meta data of the database connection used to build this Schema.
597      *
598      * @exception SQLException
599      */
600     void populate(ResultSet dbMeta)
601             throws SQLException
602     {
603         List cols = new ArrayList();
604         String tableName = null;
605         columnNumberByName = new TreeMap(String.CASE_INSENSITIVE_ORDER);
606 
607         while (dbMeta.next())
608         {
609             if (tableName == null)
610             {
611                 tableName = dbMeta.getString(3); // table name
612                 setTableName(tableName);
613             }
614             else if (!tableName.equals(dbMeta.getString(3))) // not same table name
615             {
616                 dbMeta.previous(); // reset result set pointer
617                 break;
618             }
619 
620             Column c = new Column();
621 
622             c.populate(tableName,
623                 dbMeta.getString(4), // column name
624                 dbMeta.getString(6), // Data source dependent type name
625                 dbMeta.getInt(5), // SQL type from java.sql.Types
626                 dbMeta.getInt(11) == DatabaseMetaData.columnNullable); // is NULL allowed.
627 
628             cols.add(c);
629 
630             Integer position = new Integer(dbMeta.getInt(17)); // ordinal number
631             columnNumberByName.put(c.name(), position);
632             columnNumberByName.put(tableName + "." + c.name(), position);
633         }
634 
635         if (!cols.isEmpty())
636         {
637             this.numberOfColumns = cols.size();
638             columns = new Column[numberOfColumns() + 1];
639 
640             int i = 1;
641             for (Iterator col = cols.iterator(); col.hasNext();)
642             {
643                 columns[i++] = (Column) col.next();
644             }
645         }
646     }
647 
648     /***
649      * Sets the columns to select from the table
650      *
651      * @param attributes comma separated list of column names
652      */
653     void setAttributes(String attributes)
654     {
655         this.columnsAttribute = attributes;
656     }
657 
658     /***
659      * Sets the table name that this Schema represents
660      *
661      * @param tableName
662      */
663     void setTableName(String tableName)
664     {
665         this.tableName = tableName;
666     }
667 
668     /***
669      * returns the table name that this Schema represents
670      *
671      * @return the table name that this Schema represents
672      *
673      * @throws DataSetException TODO: DOCUMENT ME!
674      */
675     public String tableName()
676             throws DataSetException
677     {
678         return getTableName();
679     }
680 
681     /***
682      * This returns a representation of this Schema
683      *
684      * @return a string
685      */
686     public String toString()
687     {
688         ByteArrayOutputStream bout = new ByteArrayOutputStream();
689         PrintWriter out = new PrintWriter(bout);
690         out.print('{');
691 
692         for (int i = 1; i <= numberOfColumns; i++)
693         {
694             out.print('\'');
695 
696             if (!singleTable)
697             {
698                 out.print(columns[i].getTableName() + '.');
699             }
700 
701             out.print(columns[i].name() + '\'');
702 
703             if (i < numberOfColumns)
704             {
705                 out.print(',');
706             }
707         }
708 
709         out.print('}');
710         out.flush();
711 
712         return bout.toString();
713     }
714 }