View Javadoc

1   /*
2    * Copyright 2006 Outsource Cafe, Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the 'License')
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an 'AS IS' BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.javagen.agile.db;
17  
18  import java.sql.Connection;
19  import java.sql.DatabaseMetaData;
20  import java.sql.ResultSet;
21  import java.sql.ResultSetMetaData;
22  import java.sql.SQLException;
23  import java.sql.Types;
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.StringTokenizer;
31  
32  import javax.sql.DataSource;
33  
34  import org.javagen.agile.core.Generator;
35  import org.javagen.agile.core.model.Model;
36  import org.javagen.agile.db.model.Column;
37  import org.javagen.agile.db.model.ColumnReference;
38  import org.javagen.agile.db.model.Database;
39  import org.javagen.agile.db.model.DbType;
40  import org.javagen.agile.db.model.FkConstraint;
41  import org.javagen.agile.db.model.FkEnum;
42  import org.javagen.agile.db.model.PkColumn;
43  import org.javagen.agile.db.model.Table;
44  import org.javagen.agile.db.model.UniqueConstraint;
45  import org.javagen.agile.db.util.DbUtil;
46  
47  /***
48   * Create DB model tree from JDBC metadata from provided DataSource.
49   * <p>
50   * The scope of the model tree can be limited by specifying a list of 
51   * <code>includeTableNames</code> and/or <code>excludeTableNames</code>, otherwise
52   * all table schemas are loaded.
53   *
54   * @author Richard Easterling
55   */
56  public class DbLoader implements Generator {
57  
58      private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(DbLoader.class);
59  	private static final int CASCADE = DatabaseMetaData.importedKeyCascade; 
60  	protected DataSource dataSource;
61      protected List<String> includeTableNames;
62      protected Set<String> excludeTableNames;
63  	protected Model parentModel;
64      protected boolean namesToUpperCase = false;
65  
66  	////////////////////////////////////////////////////////////////////////////
67  	// Generator interface:
68  	////////////////////////////////////////////////////////////////////////////
69  	
70  	/***
71       * Create DB model tree from JDBC metadata from provided DataSource.
72       * 
73       * @param input can be one of three possibilities:
74       * <ol>
75       * <li>Database instance - provided database is processed instead of creating a new instance.</li>
76       * <li>A non-Database instance - input model is treated as container for new Database instance.</li>
77       * <li>null - new Database is created.</li>
78       * </ol>
79       * @return container model if present, otherwise return the Database instance.
80  	 */
81      public Model gen(Model input) {  
82          Database database = null;
83          Model result = null;
84          if ( input!=null && (input instanceof Database) ) {
85              //database instance provided, try to find a parent container to add it to
86              database = (Database)input;
87              if (parentModel!=null) {
88                  parentModel.addChildModel(database);
89                  result = parentModel;
90              } else {
91                  result = database;
92              }
93          } else {
94              //create a new database instance and try to find a parent container to add it to
95              database = new Database(Database.DEFAULT_MODEL_TYPE);
96              if (parentModel!=null) {
97                  parentModel.addChildModel(database);
98                  result = parentModel;
99              } else if (input!=null) {
100                 input.addChildModel(database);
101                 result = input;
102             } else {
103                 result = database;
104             }
105         }
106         try {
107         	load(database);
108         } catch (SQLException e) {
109         	throw new RuntimeException(e);
110         }
111         return result;
112 	}
113 
114 	////////////////////////////////////////////////////////////////////////////
115 	// Walk DB Schema and create Database tree:
116 	////////////////////////////////////////////////////////////////////////////
117 	
118     /***
119      * Load tables into Database instance using supplied DataSource.
120      */
121 	public void load(Database database) throws SQLException {
122 		Connection dbconn = null;
123 		List<String> tableList = includeTableNames==null ? null : includeTableNames;
124 		try {
125 			dbconn = dataSource.getConnection();
126 			DatabaseMetaData dbMeta = dbconn.getMetaData();
127 //			if (database.getName()==null)
128 //				database.setName(dbMeta.getURL());
129 			if (tableList==null) {
130 				tableList = loadTableList(dbMeta, database);
131             } else {
132                 verifyTableList(dbMeta, database, tableList);
133             }
134 			loadTables(dbconn, dbMeta, database, tableList); //first pass loads tables/columns
135 			loadImportedKeys(dbMeta, database, tableList); //second pass wires up foreign key relationships
136 		} finally {
137 			if (dbconn!=null) 
138 				dbconn.close();			
139 		}
140 	}
141 
142     /*** 
143      * Create list of all table names from DatabaseMetaData.
144      */
145     public List<String> loadTableList(DatabaseMetaData dbMeta, Database database) throws SQLException {
146         String[] tableTypes = {"TABLE"}; //"TABLE", "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"
147         List<String> tableList = new ArrayList<String>();
148         ResultSet tables = dbMeta.getTables(database.getCatalog(), database.getSchema(), null, tableTypes);
149         while (tables.next()) {
150             String tableName = tables.getString("TABLE_NAME");
151             if (this.namesToUpperCase)
152                 tableName = tableName.toUpperCase(); 
153 //          String catalog = tables.getString("TABLE_CAT");
154 //          String schema = tables.getString("TABLE_SCHEM");
155 //          String tableType = tables.getString("TABLE_TYPE");
156 //          String tableRemarks = tables.getString("REMARKS");
157             if (excludeTableNames==null || !excludeTableNames.contains(tableName))
158                 tableList.add(tableName);
159         }
160         tables.close();
161         return tableList;
162     }
163     
164     /*** 
165      * Verify existence of provided table list.
166      */
167     public void verifyTableList(DatabaseMetaData dbMeta, Database database, List<String> tableList) throws SQLException {
168         String[] tableTypes = {"TABLE"}; //"TABLE", "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM"
169         for (String tableName : tableList) {
170             ResultSet tables = dbMeta.getTables(database.getCatalog(), database.getSchema(), tableName, tableTypes);
171             boolean OK = tables.next();
172             tables.close();
173             if (!OK) {
174                 throw new IllegalArgumentException("Provided table name '"+tableName+"' not in database");
175             }
176         }
177     }
178     
179 	/***
180 	 * Populate column data for this table obtained from DatabaseMetaData.getColumns() ResultSet.
181 	 * @todo add support for versioning by calling DatabaseMetaData.getVersionColumns()
182 	 */
183 	public void loadTables(Connection dbconn, DatabaseMetaData dbMeta, Database database, List<String> tableList) {
184 		try {
185 			for(String tableName : tableList) {
186 				Table table = database.lookupTable(tableName); //table may already be present from serialized metadata
187 				if (table==null) {
188 					table = database.createTable();
189 					table.setName(tableName);
190 					database.addTable(table);
191 				}
192 				//preload primary key columns
193 				Map<String, Short> pkMap = new HashMap<String, Short>();
194 				ResultSet pkeys = dbMeta.getPrimaryKeys(null, database.getSchema(), tableName);
195 				while (pkeys.next()) {
196 					String pkColumnName = pkeys.getString("COLUMN_NAME");
197 					Short seq = pkeys.getShort("KEY_SEQ");
198 					pkMap.put(pkColumnName, seq);
199 					Column col = table.lookupColumn(pkColumnName); //column may already be present from serialized metadata
200 					PkColumn pkColumn = (PkColumn)col;
201 					if (pkColumn==null) {
202 						pkColumn = table.createPkColumn();
203 						pkColumn.setName(pkColumnName);
204 						table.addColumn(pkColumn);
205 					}
206 					pkColumn.setSequenceNumber(seq);
207 				}
208 				pkeys.close();
209 
210                 //load and set attributes of all columns
211 				ResultSet columns = dbMeta.getColumns(database.getCatalog(), database.getSchema(), tableName, null);
212 
213                 int columnCount = 0;
214 				StringBuffer logSB = log.isDebugEnabled() ? new StringBuffer(tableName+" table's columns: ") : null;
215 				while (columns.next()) {
216 					++columnCount;
217 					String colName = columns.getString("COLUMN_NAME");
218 					short colType = columns.getShort("DATA_TYPE");
219 					int colSize = columns.getInt("COLUMN_SIZE");
220 					String colTypeName = columns.getString("TYPE_NAME").toUpperCase();
221 					if (logSB != null)
222 						logSB.append(colName + ":" + colTypeName + ", ");
223 					int scale = (colType == Types.DECIMAL || colType == Types.NUMERIC) ? columns.getInt("DECIMAL_DIGITS") : 0;
224 					boolean nullable = (columns.getInt("NULLABLE") == DatabaseMetaData.columnNullable) ? true : false;
225 					Object defaultValue = columns.getObject("COLUMN_DEF");
226 					Column column = table.lookupColumn(colName); //column may already be present
227 					if (column==null) {
228 						column = table.createColumn();
229 						column.setName(colName);
230 						table.addColumn(column);
231 					}
232 					column.setColumnSize(colSize==0 ? null : colSize);
233 					column.setDbType(DbType.lookup(colType));
234 					column.setNotNull(nullable ? null : true);
235 					column.setScale(scale==0 ? null : scale);
236 					column.setDefaultValue(defaultValue);
237 					column.setDbTypeName(colTypeName);
238 				}
239 				columns.close();
240                 if (logSB!=null)
241                     log.info(logSB.toString());
242                 
243                 //look for auto increment keys - is there a pure metadata way to do this?
244                 ResultSetMetaData columnMetaData = dbconn.createStatement().executeQuery("select * from "+tableName+" where 1=0").getMetaData();
245                 for(int i=1;i<=columnMetaData.getColumnCount();i++)
246                     if (columnMetaData.isAutoIncrement(i)) {
247                         log.info("auto increment column found:"+tableName+"."+columnMetaData.getColumnName(i));
248                         Column autoColumn = table.lookupColumn(columnMetaData.getColumnName(i));
249                         autoColumn.setAutoIncrement(true);
250                     }
251                 
252                 //create unique constraints
253                 Map<String,UniqueConstraint> uniqueConstraintsMap =  new HashMap<String,UniqueConstraint>();
254                 ResultSet uniqueIndexes = dbMeta.getIndexInfo(database.getCatalog(), database.getSchema(), tableName, true, false);
255                 while (uniqueIndexes.next()) {
256                     String columnName = uniqueIndexes.getString("COLUMN_NAME");
257                     Boolean nonUnique = uniqueIndexes.getBoolean("NON_UNIQUE");
258                     String indexName = uniqueIndexes.getString("INDEX_NAME");
259                     if (nonUnique!=null && !nonUnique) {
260                         Column column = table.lookupColumn(columnName);
261                         UniqueConstraint uniqueConstraint = uniqueConstraintsMap.get(indexName);
262                         if (uniqueConstraint==null) {
263                             uniqueConstraint = new UniqueConstraint();
264                             uniqueConstraintsMap.put(indexName, uniqueConstraint);
265                             uniqueConstraint.setName(indexName);
266                         }
267                         uniqueConstraint.addColumn(column);
268                     }
269                 }
270                 uniqueIndexes.close();
271                 //now add UniqueConstraint references to effected column contexts for easy reference
272                 for(UniqueConstraint uniqueConstraint : uniqueConstraintsMap.values())
273                     if (uniqueConstraint.columnReferencesSize()==1)
274                         uniqueConstraint.getColumns().get(0).setUnique(true);
275                 
276                 
277                 //look for idnentity columns
278 //                columns = dbMeta.getColumns(database.getCatalog(), database.getSchema(), "SYSTEM_SEQUENCES", null);
279 //                while (columns.next()) {
280 //                    String colName = columns.getString("SEQUENCE_NAME");
281 //                    System.out.println("SEQUENCE_NAME="+colName);
282 //                }
283 //                columns.close();
284                 
285 //              HSQL SYSTEM_SEQUENCES
286 //              You can query the SYSTEM_SEQUENCES table for the next value that will be returned from any of 
287 //              the defined sequences. The SEQUENCE_NAME column contains the name and the NEXT_VALUE 
288 //              column contains the next value to be returned. 
289             
290 
291                 
292 			} // Table loop
293 		} catch (SQLException e) {
294 			throw new RuntimeException(e);
295 		}
296 	}
297 
298     /***
299      * Wire up foreign key relationships 
300      * @param dbMeta DatabaseMetaData instance
301      * @param database root model node of DB tree
302      * @param tableSet Mechinism for mapping subset of tables - FKs to tables not in this list are just treated like data columns. 
303      * @throws java.sql.SQLException
304      */
305 	public void loadImportedKeys(DatabaseMetaData dbMeta, Database database, List<String> tableList) throws java.sql.SQLException
306 	{
307         Set<String> tableSet = new HashSet<String>(tableList);
308 		for(String tableName : tableSet) {
309 			ResultSet fkeys = dbMeta.getImportedKeys(null, database.getSchema(), tableName);
310 			Table localTable = database.lookupTable(tableName);
311 			StringBuffer logSB = log.isDebugEnabled() ? new StringBuffer(tableName+" table's imported foreign keys: ") : null;
312 			while (fkeys.next()) {
313 				String pkTableName    = fkeys.getString("PKTABLE_NAME");
314 				String pkColName      = fkeys.getString("PKCOLUMN_NAME");
315 				String fkTableName    = fkeys.getString("FKTABLE_NAME");
316 				String fkColName      = fkeys.getString("FKCOLUMN_NAME");
317 				short seq             = fkeys.getShort("KEY_SEQ");
318                 String fkName         = fkeys.getString("FK_NAME");
319                 boolean cascadeUpdate = CASCADE == fkeys.getInt("UPDATE_RULE");
320                 boolean cascadeDelete = CASCADE == fkeys.getInt("DELETE_RULE");
321 				boolean skipFk = tableSet!=null && !tableSet.contains(pkTableName);
322 				if (logSB!=null) {
323 					logSB.append(skipFk ? "SKIP>" : "FK").append(fkTableName).append(".").append(fkColName).append("[").append(seq).append("], ");
324 				}
325 				if (!skipFk) {
326 					FkConstraint fkConstraint = localTable.lookupFkColumn(fkName);
327 					Table foreignTable = database.lookupTable(pkTableName);
328 					Column foreignColumn = foreignTable.lookupColumn(pkColName);
329 					Column localColumn = localTable.lookupColumn(fkColName);
330 					if (fkConstraint==null) {
331 						fkConstraint = localTable.createFkColumn();
332 						fkConstraint.setName(fkName);
333                         localTable.addFkColumn(fkConstraint);
334 					}
335 					//fkColumn.setParentTable(localTable);
336 					fkConstraint.setFkType(FkEnum.IMPORTED);
337 					fkConstraint.setTargetTable(foreignTable);
338                     if (cascadeUpdate) fkConstraint.setCascadeUpdate(cascadeUpdate);
339                     if (cascadeDelete) fkConstraint.setCascadeDelete(cascadeDelete);
340 					ColumnReference colRef = fkConstraint.lookupColumnReference(pkColName, fkColName);
341 					if (colRef==null) {
342 						colRef = fkConstraint.createColumnReference();
343 						fkConstraint.addColumnReference(colRef);
344 					}
345 					colRef.setForeignColumn(foreignColumn);
346 					colRef.setLocalColumn(localColumn);
347 					colRef.setSequenceNumber(seq);
348                     localColumn.put(ColumnReference.DEFAULT_MODEL_TYPE, colRef); //this identifies column as a FK
349                     assert fkConstraint.getParentTable()!=null;
350                     assert fkConstraint.getTargetTable()!=null;
351                     
352 				}
353 			}
354 			fkeys.close();
355 			if (logSB!=null) {
356 				log.info(logSB.toString());
357 			}
358             //create reverse (i.e. exported) relationships
359             FkConstraint[] importedFkColumns = localTable.getFkColumns().toArray(new FkConstraint[localTable.getFkColumns().size()]);
360             for(FkConstraint fkConstraint : importedFkColumns) {
361                 try {
362                     FkConstraint revFfColumn = DbUtil.createReverse(fkConstraint, true);
363                     assert revFfColumn.getParentTable()!=null;
364                     assert revFfColumn.getTargetTable()!=null;
365                 } catch (Exception e) {
366                     e.printStackTrace();
367                 }
368             }
369             
370             
371             
372 		} //tableName loop
373         
374 //		PKTABLE_CAT,PKTABLE_SCHEM, PKTABLE_NAME, PKCOLUMN_NAME, FKTABLE_CAT, FKTABLE_SCHEM, FKTABLE_NAME, FKCOLUMN_NAME, 	KEY_SEQ, UPDATE_RULE, DELETE_RULE, FK_NAME,   PK_NAME,               DEFERRABILITY, 
375 //		null, 		PUBLIC, 	   PERSONS,      PERSON_ID, 	 null, 		 PUBLIC, 	    VEHICLES,    MODIFY_PERSON_ID, 	1, 	 	 3, 			3, 	   	   SYS_FK_98, SYS_REF_SYS_FK_98_105, 7, 
376 //		null, 		PUBLIC, 	   PERSONS,      PERSON_ID, 	 null, 		 PUBLIC, 	    VEHICLES,    CREATE_PERSON_ID, 	1, 	 	 3, 			3,  	   SYS_FK_97, SYS_REF_SYS_FK_97_103, 7, 
377 //		null, 		PUBLIC, 	   ZIPCODES,     ZIPCODE, 	     null, 		 PUBLIC, 	    VEHICLES,  REGISTRATION_ZIPCODE,1, 	 	 3, 			3, 	       SYS_FK_96, SYS_REF_SYS_FK_96_101, 7, 
378 	}
379 
380     ////////////////////////////////////////////////////////////////////////////
381     // Injected getters and setters:
382     ////////////////////////////////////////////////////////////////////////////
383     
384     public List<String> getIncludeTableNames() {
385         return includeTableNames;
386     }
387 
388     public void setIncludeTableNames(List<String> tableNames) {
389         if (namesToUpperCase && tableNames!=null) {
390             for(int i=0;i<tableNames.size();i++)
391                 tableNames.set(i, tableNames.get(i).toUpperCase());
392         }
393         this.includeTableNames = tableNames;
394     }
395 
396     public DataSource getDataSource() {
397         return dataSource;
398     }
399 
400     public void setDataSource(DataSource dataSource) {
401         this.dataSource = dataSource;
402     }
403 
404     public Model getParentModel() {
405         return parentModel;
406     }
407 
408     public void setParentModel(Model baseModel) {
409         this.parentModel = baseModel;
410     }
411 
412     public boolean isNamesToUpperCase() {
413         return namesToUpperCase;
414     }
415 
416     public void setNamesToUpperCase(boolean namesToUpperCase) {
417         this.namesToUpperCase = namesToUpperCase;
418     }
419 
420     public Set<String> getExcludeTableNames() {
421         return excludeTableNames;
422     }
423 
424     public void setExcludeTableNames(Set<String> excludeTableNames) {
425         if (excludeTableNames==null) {
426             this.excludeTableNames = null;
427         } else {
428             this.excludeTableNames = new HashSet<String>();
429             for(String name : excludeTableNames)
430                 this.excludeTableNames.add( this.namesToUpperCase ? name.toUpperCase() : name );
431         }
432     }
433 
434     /***
435      * White-space/comma delineated list of table names to include in the transformation process.
436      */
437     public void setIncludeTables(String delineatedIncludeTables) {
438         if (delineatedIncludeTables==null || delineatedIncludeTables.trim().length()==0)
439             return;
440         List<String> included = new ArrayList<String>();
441         StringTokenizer toks = new StringTokenizer(delineatedIncludeTables,", ", false);
442         while(toks.hasMoreTokens())
443             included.add( toks.nextToken() );
444         setIncludeTableNames(included);
445     }
446 
447     /***
448      * White-space/comma delineated list of table names to exclude from the transformation process.
449      */
450     public void setExcludeTables(String delineatedExcludeTables) {
451         if (delineatedExcludeTables==null || delineatedExcludeTables.trim().length()==0)
452             return;
453         Set<String> excluded = new HashSet<String>();
454         StringTokenizer toks = new StringTokenizer(delineatedExcludeTables,", ", false);
455         while(toks.hasMoreTokens())
456             excluded.add( toks.nextToken() );
457         setExcludeTableNames(excluded);
458     }
459 
460 }