View Javadoc

1   /* Version 1.0 based on Apache Software License 1.1
2    *
3    * Copyright (c) 2003 Piotr Maj and DBMonster developers. All rights
4    * reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions are
8    * met:
9    *
10   * 1. Redistributions of source code must retain the above copyright
11   *    notice, this list of conditions and the following disclaimer.
12   *
13   * 2. Redistributions in binary form must reproduce the above copyright
14   *    notice, this list of conditions and the following disclaimer in the
15   *    documentation and/or other materials provided with the distribution.
16   *
17   * 3. The end-user documentation included with the redistribution, if any,
18   *    must include the following acknowledgment:
19   *
20   *    "This product includes software developed by DBMonster developers
21   *    (http://dbmonster.kernelpanic.pl/)."
22   *
23   *  Alternately, this acknowledgment may appear in the software itself,
24   *  if and wherever such third-party acknowledgments normally appear.
25   *
26   * 4. The name "DBMonster" must not be used to endorse or promote products
27   *    derived from this software without prior written permission. For
28   *    written permission, please contact piotr.maj@kernelpanic.pl.
29   *
30   * 5. Products derived from this software may not be called "DBMonster",
31   *    nor may "DBMonster" appear in their name, without prior written
32   *    permission of Piotr Maj.
33   *
34   * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
35   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
36   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
37   * IN NO EVENT SHALL THE DBMONSTER DEVELOPERS BE LIABLE FOR ANY DIRECT,
38   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
39   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
40   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
41   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
42   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
43   * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
44   * POSSIBILITY OF SUCH DAMAGE.
45   */
46  
47  package pl.kernelpanic.dbmonster;
48  
49  import java.io.File;
50  import java.io.FileOutputStream;
51  import java.io.IOException;
52  import java.io.OutputStream;
53  import java.io.PrintWriter;
54  import java.sql.Connection;
55  import java.sql.DatabaseMetaData;
56  import java.sql.ResultSet;
57  import java.sql.SQLException;
58  import java.sql.Types;
59  import java.util.HashMap;
60  import java.util.Iterator;
61  import java.util.Map;
62  import java.util.Properties;
63  import java.util.Vector;
64  
65  import org.apache.commons.logging.Log;
66  import org.apache.commons.logging.LogFactory;
67  
68  import pl.kernelpanic.dbmonster.connection.ConnectionProvider;
69  import pl.kernelpanic.dbmonster.connection.DBCPConnectionProvider;
70  import pl.kernelpanic.dbmonster.connection.Transaction;
71  import pl.kernelpanic.dbmonster.generator.BooleanGenerator;
72  import pl.kernelpanic.dbmonster.generator.DataGenerator;
73  import pl.kernelpanic.dbmonster.generator.DateTimeGenerator;
74  import pl.kernelpanic.dbmonster.generator.ForeignKeyGenerator;
75  import pl.kernelpanic.dbmonster.generator.KeyGenerator;
76  import pl.kernelpanic.dbmonster.generator.MaxKeyGenerator;
77  import pl.kernelpanic.dbmonster.generator.NullGenerator;
78  import pl.kernelpanic.dbmonster.generator.NumberGenerator;
79  import pl.kernelpanic.dbmonster.generator.StringGenerator;
80  import pl.kernelpanic.dbmonster.generator.StringKeyGenerator;
81  import pl.kernelpanic.dbmonster.schema.Column;
82  import pl.kernelpanic.dbmonster.schema.Key;
83  import pl.kernelpanic.dbmonster.schema.Schema;
84  import pl.kernelpanic.dbmonster.schema.SchemaException;
85  import pl.kernelpanic.dbmonster.schema.SchemaUtil;
86  import pl.kernelpanic.dbmonster.schema.Table;
87  import pl.kernelpanic.dbmonster.sql.ExtendedTypes;
88  
89  /***
90   * Schema Grabber.
91   *
92   * @author Piotr Maj <piotr.maj@kernelpanic.pl>
93   *
94   * @version $Id: SchemaGrabber.html,v 1.1 2007/06/21 08:38:14 sbahloul Exp $
95   */
96  public class SchemaGrabber {
97  
98      /***
99       * Logger.
100      */
101     private Log log = LogFactory.getLog(SchemaGrabber.class);
102 
103     /***
104      * Connection provider.
105      */
106     private ConnectionProvider connectionProvider = null;
107 
108     /***
109      * Specifies the tables we want to grab. If set to <code>null</code>
110      * all tables will be grabbed.
111      */
112     private Vector tables = null;
113 
114     /***
115      * Output stream.
116      */
117     private OutputStream output = System.out;
118 
119     /***
120      * Properties.
121      */
122     private Properties properties = new Properties();
123 
124     /***
125      * Database schema used in grabbing table. Useful for Oracle,
126      * and other databases that support schema.
127      */
128     private String dbSchema = null;
129 
130     /***
131      * Number of rows to generate.
132      */
133     private int numRows = 1000;
134 
135     /***
136      * Constructs new SchemaGrabber.
137      */
138     public SchemaGrabber() {
139     }
140 
141     /***
142      * Starts SchemaGrabber
143      *
144      * @param args command line arguments
145      *
146      * @throws Exception on errors
147      */
148     public static void main(final String[] args) throws Exception {
149         SchemaGrabber sg = new SchemaGrabber();
150         ConnectionProvider cp = new DBCPConnectionProvider();
151         sg.setConnectionProvider(cp);
152         Schema schema = sg.grabSchema();
153         OutputStream os = sg.getOutput();
154         SchemaUtil.serializeSchema(new PrintWriter(os), schema);
155     }
156 
157     /***
158      * Starts schema grabber.
159      *
160      * @throws Exception on errors
161      */
162     public final void doTheJob() throws Exception {
163         Schema schema = grabSchema();
164         SchemaUtil.serializeSchema(new PrintWriter(output), schema);
165     }
166 
167     /***
168      * Returns the output stream where the serialized schema should
169      * be pushed.
170      *
171      * @return output stream
172      */
173     public final OutputStream getOutput() {
174         return output;
175     }
176 
177     /***
178      * Grabs the schema.
179      *
180      * @return Schema schema
181      *
182      * @throws Exception on errors
183      */
184     public final Schema grabSchema() throws Exception {
185 
186         // read needed properties first.
187         dbSchema = properties.getProperty("dbmonster.jdbc.schema", null);
188         if (dbSchema != null && "".equals(dbSchema)) {
189             dbSchema = null;
190         }
191         String rows = properties.getProperty("dbmonster.rows", "1000");
192         try {
193             numRows = Integer.valueOf(rows).intValue();
194         } catch (Exception e) {
195         }
196         Schema schema = new Schema();
197         schema.setName("Change me!");
198         if (tables == null) {
199             tables = getTableNames();
200         }
201         Iterator it = tables.iterator();
202         int count = tables.size();
203         int current = 1;
204         log.info("Grabbing schema from database. "
205             + count + " tables to grab.");
206         while (it.hasNext()) {
207             String tableName = (String) it.next();
208             Table t = grabTable(tableName);
209             schema.addTable(t);
210             log.info("Grabbing table " + tableName + ". "
211                  + ((current * 100) / count) + "% done.");
212             ++current;
213         }
214         log.info("Grabbing schema from database complete.");
215         return schema;
216     }
217 
218     /***
219      * Grabs the table.
220      *
221      * @param name table name
222      *
223      * @return table
224      *
225      * @throws SQLException on SQL errors
226      * @throws SchemaException on schema errors
227      */
228     public final Table grabTable(final String name)
229         throws SQLException, SchemaException {
230         ResultSet rs;
231 
232         Table t = new Table();
233         String tableName = name;
234         if (dbSchema != null) {
235             tableName = dbSchema + "." + tableName;
236         }
237         t.setName(tableName);
238         t.setRows(numRows);
239         Transaction tx = null;
240         try {
241             tx = new Transaction(connectionProvider);
242             Connection conn = tx.begin();
243             DatabaseMetaData md = conn.getMetaData();
244 
245             // key
246             Vector keyColumns = new Vector();
247             rs = md.getPrimaryKeys(null, dbSchema, name);
248             while (rs.next()) {
249                 String columnName = rs.getString("COLUMN_NAME");
250                 keyColumns.add(columnName);
251             }
252             KeyGenerator generator = suggestKeyGenerator(name, keyColumns);
253             if (generator != null) {
254                 Key key = new Key();
255                 key.setGenerator(generator);
256                 t.setKey(key);
257             }
258             else {
259                 // This is to import anyway multiple keys.
260                 keyColumns.clear();
261             }
262 
263             //imported keys
264             HashMap foreignKeys = new HashMap();
265             rs = md.getImportedKeys(null, null, name);
266             while (rs.next()) {
267                 String fkColumn = rs.getString("FKCOLUMN_NAME");
268                 String pkColumn = rs.getString("PKCOLUMN_NAME");
269                 String pkTable = rs.getString("PKTABLE_NAME");
270 /*                String pkSchema = rs.getString("PKTABLE_SCHEM");
271 // Removed! Tables for ForeignKeyGenerator are not found if these lines
272 // are active!
273                 if (pkSchema != null) {
274                     pkTable = pkSchema + "." + pkTable;
275                 }*/
276                 foreignKeys.put(fkColumn, new PairBean(pkTable, pkColumn));
277             }
278 
279             // columns
280             rs = md.getColumns(null, dbSchema, name, "%");
281             while (rs.next()) {
282                 String columnName = rs.getString("COLUMN_NAME");
283                 if (keyColumns.contains(columnName)) {
284                     continue;
285                 }
286                 Column c = new Column();
287                 c.setName(columnName);
288                 c.setGenerator(suggestGenerator(name, columnName, foreignKeys));
289                 t.addColumn(c);
290             }
291 
292         } catch (SQLException e) {
293             tx.abort();
294             log.fatal(e.getMessage());
295             throw e;
296         } finally {
297             if (tx != null) {
298                 tx.close();
299             }
300         }
301         return t;
302     }
303 
304     /***
305      * Returns names of the tables in the database.
306      *
307      * @return vector of strings
308      *
309      * @throws SQLException on errors
310      */
311     public final Vector getTableNames() throws SQLException {
312         Vector v = new Vector();
313         Transaction tx = null;
314         try {
315             tx = new Transaction(connectionProvider);
316             Connection conn = tx.begin();
317             DatabaseMetaData md = conn.getMetaData();
318             ResultSet rs =
319                 md.getTables(null, dbSchema, "%", new String[] {"TABLE"});
320             while (rs.next()) {
321                 String name = rs.getString("TABLE_NAME");
322                 v.add(name);
323             }
324             tx.commit();
325         } catch (SQLException e) {
326             log.fatal(e.getMessage());
327             tx.abort();
328             throw e;
329         } finally {
330             if (tx != null) {
331                 tx.close();
332             }
333         }
334         return v;
335     }
336 
337     /***
338      * Returns a connection provider used by the grabber.
339      *
340      * @return connection provider instance
341      */
342     public final ConnectionProvider getConnectionProvider() {
343         return connectionProvider;
344     }
345 
346     /***
347      * Sets the connection provider.
348      *
349      * @param provider connection provider
350      */
351     public final void setConnectionProvider(
352         final ConnectionProvider provider) {
353         connectionProvider = provider;
354     }
355 
356     /***
357      * Returns the logger.
358      *
359      * @return logger
360      */
361     public final Log getLog() {
362         return log;
363     }
364 
365     /***
366      * Sets the logger
367      *
368      * @param logger new logger
369      */
370     public final void setLog(final Log logger) {
371         log = logger;
372     }
373 
374     /***
375      * Adds a table names to the list of grabbed tables.
376      *
377      * @param name name of the table
378      *
379      * @throws SQLException if table cannot be found
380      */
381     public final void addTable(final String name) throws SQLException {
382         if (tables == null) {
383             tables = new Vector();
384         }
385         String tableName = name;
386         Transaction tx = null;
387         try {
388             tx = new Transaction(connectionProvider);
389             Connection conn = tx.begin();
390             DatabaseMetaData md = conn.getMetaData();
391             if (md.storesLowerCaseIdentifiers()) {
392                 tableName = name.toLowerCase();
393             } else if (md.storesUpperCaseIdentifiers()) {
394                 tableName = name.toUpperCase();
395             }
396             ResultSet rs =
397                 md.getTables(null, dbSchema, tableName, new String[] {"TABLE"});
398             if (!rs.next()) {
399                 throw new SQLException("No such table <" + name + ">.");
400             }
401             tx.commit();
402             tables.add(tableName);
403         } catch (SQLException e) {
404             log.fatal(e.getMessage());
405             tx.abort();
406             throw e;
407         } finally {
408             if (tx != null) {
409                 tx.close();
410             }
411         }
412     }
413 
414     /***
415      * Sets the output file name.
416      *
417      * @param file file name
418      *
419      * @throws IOException on I/O errors
420      */
421     public final void setOutputFile(final String file) throws IOException {
422         File f = new File(file);
423         output = new FileOutputStream(f);
424     }
425 
426     /***
427      * Sets the properties for the SchemaGrabber
428      * @param p
429      */
430     public final void setProperties(final Properties p) {
431         properties = p;
432     }
433 
434     /***
435      * Suggests which key generator should be used. In current implementation
436      * it only check if the key consists of only one table and if so it uses
437      * MaxKeyGenerator (it the type of the column allow it).
438      *
439      * @param tableName the name ot the table
440      * @param columns columns
441      *
442      * @return key generator or <code>null</code>
443      *
444      * @throws SQLException on errors
445      */
446     private final KeyGenerator suggestKeyGenerator(
447         final String tableName,
448         final Vector columns) throws SQLException {
449 
450         KeyGenerator generator = null;
451 
452         if (columns.size() != 1) {
453             return generator;
454         }
455         String columnName = (String) columns.get(0);
456         Transaction tx = null;
457         try {
458             tx = new Transaction(connectionProvider);
459             Connection conn = tx.begin();
460             DatabaseMetaData md = conn.getMetaData();
461             ResultSet rs = md.getColumns(null, null, tableName, columnName);
462             rs.next();
463             int dataType = rs.getInt("DATA_TYPE");
464             if (isIntegerType(dataType)) {
465                 generator = new MaxKeyGenerator();
466                 ((MaxKeyGenerator) generator).setColumnName(columnName);
467             } else if (isTextType(dataType)) {
468                 generator = new StringKeyGenerator();
469                 ((StringKeyGenerator) generator).setStartValue("0");
470                 ((StringKeyGenerator) generator).setColumnName(columnName);
471             } else {
472                 if (log.isWarnEnabled()) {
473                     log.warn("Datatype " + dataType + " for " + columnName
474                         + " is unknown, no Key Generator in schema.xml");
475                 }
476             }
477             tx.commit();
478         } catch (SQLException e) {
479             tx.abort();
480             log.fatal(e.getMessage());
481             throw e;
482         } finally {
483             if (tx != null) {
484                 tx.close();
485             }
486         }
487         return generator;
488     }
489 
490     /***
491      * Suggests the data generator for a column.
492      *
493      * @param tableName table name
494      * @param columnName column name
495      * @param foreignKeys infomation about foreing keys in this table
496      *
497      * @return data generator. Never <code>null</code>.
498      *
499      * @throws SQLException on SQL errors
500      */
501     private final DataGenerator suggestGenerator(
502         final String tableName,
503         final String columnName,
504         final Map foreignKeys) throws SQLException {
505 
506         DataGenerator generator = new NullGenerator();
507         if (foreignKeys.containsKey(columnName)) {
508             generator = new ForeignKeyGenerator();
509             PairBean bean = (PairBean) foreignKeys.get(columnName);
510             ((ForeignKeyGenerator) generator).setTableName(bean.getKey());
511             ((ForeignKeyGenerator) generator).setColumnName(bean.getValue());
512             return generator;
513         }
514         Transaction tx = null;
515         try {
516             tx = new Transaction(connectionProvider);
517             Connection conn = tx.begin();
518             DatabaseMetaData md = conn.getMetaData();
519             ResultSet rs = md.getColumns(null, null, tableName, columnName);
520             rs.next();
521             int dataType = rs.getInt("DATA_TYPE");
522             String isNullable = rs.getString("IS_NULLABLE");
523             int nulls = 0;
524             if (isNullable == null) {
525                 isNullable = "NO";
526             }
527             if ("YES".equals(isNullable)) {
528                 nulls = 10;
529             }
530             int columnSize = rs.getInt("COLUMN_SIZE");
531             int decimalDigits = rs.getInt("DECIMAL_DIGITS");
532             if (isIntegerType(dataType)) {
533                 generator = new NumberGenerator();
534                 NumberGenerator numGen = (NumberGenerator) generator;
535                 numGen.setNulls(nulls);
536                 if (dataType == Types.BIGINT) {
537                     numGen.setReturnedType("long");
538                 } else if (dataType == Types.INTEGER) {
539                     numGen.setReturnedType("integer");
540                 } else if (dataType == Types.SMALLINT
541                     || dataType == Types.TINYINT) {
542                     numGen.setReturnedType("short");
543                 } else if (dataType == Types.DECIMAL || dataType == Types.NUMERIC) {
544                     if (decimalDigits > 0) {
545                         numGen.setReturnedType("float");
546                         numGen.setScale(decimalDigits);
547                     } else {
548                         numGen.setReturnedType("integer");
549                     }
550                 }
551             } else if (isBooleanType(dataType)) {
552                 generator = new BooleanGenerator();
553                 ((BooleanGenerator) generator).setNulls(nulls);
554             } else if (isTextType(dataType)) {
555                 generator = new StringGenerator();
556                 ((StringGenerator) generator).setNulls(nulls);
557                 if (columnSize > 0) {
558                     ((StringGenerator) generator).setMaxLength(columnSize);
559                 }
560             } else if (isTimeType(dataType)) {
561                 generator = new DateTimeGenerator();
562                 ((DateTimeGenerator) generator).setNulls(nulls);
563                 if (dataType == Types.DATE) {
564                     ((DateTimeGenerator) generator).setReturnedType("date");
565                 } else if (dataType == Types.TIME) {
566                     ((DateTimeGenerator) generator).setReturnedType("time");
567                 }  else if (dataType == Types.TIMESTAMP) {
568                     ((DateTimeGenerator) generator)
569                         .setReturnedType("timestamp");
570                 }
571             } else {
572                 if (log.isWarnEnabled()) {
573                     log.warn("Unknown datatype. No generator created.");
574                 }
575             }
576             tx.commit();
577         } finally {
578             if (tx != null) {
579                 tx.close();
580             }
581         }
582         return generator;
583     }
584 
585     /***
586      * Checks if the type is one of numeric type.
587      *
588      * @param type type to check
589      *
590      * @return <code>true</code> if type accepts numbers
591      */
592     private final boolean isIntegerType(final int type) {
593         int tempType;
594         
595         tempType = ExtendedTypes.getInstance().
596             getStandardAlias(properties.getProperty(
597             "dbmonster.jdbc.driver"), type);
598         
599         return (tempType == Types.BIGINT)
600             || (tempType == Types.INTEGER)
601             || (tempType == Types.SMALLINT)
602             || (tempType == Types.TINYINT)
603             || (tempType == Types.DECIMAL)
604             || (tempType == Types.NUMERIC);
605     }
606 
607     /***
608      * Checks if the type is one of text or character type.
609      *
610      * @param type type to check
611      *
612      * @return <code>true</code> if type accepts strings
613      */
614     private final boolean isTextType(final int type) {
615         int tempType;
616         
617         tempType = ExtendedTypes.getInstance().
618             getStandardAlias(properties.getProperty(
619             "dbmonster.jdbc.driver"), type);
620         
621         return (tempType == Types.CHAR)
622             || (tempType == Types.LONGVARCHAR)
623             || (tempType == Types.VARCHAR);
624     }
625     /***
626      * Checks if the type is one of text or date/time type.
627      *
628      * @param type type to check
629      *
630      * @return <code>true</code> if type accepts dates
631      */
632     private final boolean isTimeType(final int type) {
633         int tempType;
634         
635         tempType = ExtendedTypes.getInstance().
636             getStandardAlias(properties.getProperty(
637             "dbmonster.jdbc.driver"), type);
638         
639         return (tempType == Types.DATE)
640             || (tempType == Types.TIME)
641             || (tempType == Types.TIMESTAMP);
642     }
643 
644     /***
645      * Checks if the type is boolean
646      *
647      * @param type type to check
648      *
649      * @return <code>true</code> if type is boolean
650      */
651     private final boolean isBooleanType(final int type) {
652         int tempType;
653         
654         tempType = ExtendedTypes.getInstance().
655             getStandardAlias(properties.getProperty(
656             "dbmonster.jdbc.driver"), type);
657         
658         return (tempType == Types.BIT)
659             || (tempType == Types.BOOLEAN);
660     }
661 }
662 
663 /***
664  * Simple maps a key to a value.
665  */
666 final class PairBean {
667 
668     /***
669      * Key.
670      */
671     private String key = null;
672 
673     /***
674      * Value.
675      */
676     private String value = null;
677 
678     /***
679      * Constructs new PairBean
680      *
681      * @param k key
682      * @param v value
683      */
684     public PairBean(final String k, final String v) {
685         key = k;
686         value = v;
687     }
688 
689     /***
690      * Returns the key.
691      *
692      * @return key
693      */
694     public final String getKey() {
695         return key;
696     }
697 
698     /***
699      * Sets the key.
700      *
701      * @param k key
702      */
703     public final void setKey(final String k) {
704         key = k;
705     }
706 
707     /***
708      * Returns the value.
709      *
710      * @return value
711      */
712     public final String getValue() {
713         return value;
714     }
715 
716     /***
717      * Sets the value
718      *
719      * @param v value to set
720      */
721     public final void setValue(final String v) {
722         value = v;
723     }
724 }