1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
86 database = (Database)input;
87 if (parentModel!=null) {
88 parentModel.addChildModel(database);
89 result = parentModel;
90 } else {
91 result = database;
92 }
93 } else {
94
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
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
128
129 if (tableList==null) {
130 tableList = loadTableList(dbMeta, database);
131 } else {
132 verifyTableList(dbMeta, database, tableList);
133 }
134 loadTables(dbconn, dbMeta, database, tableList);
135 loadImportedKeys(dbMeta, database, tableList);
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"};
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
154
155
156
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"};
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);
187 if (table==null) {
188 table = database.createTable();
189 table.setName(tableName);
190 database.addTable(table);
191 }
192
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);
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
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);
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
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
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
272 for(UniqueConstraint uniqueConstraint : uniqueConstraintsMap.values())
273 if (uniqueConstraint.columnReferencesSize()==1)
274 uniqueConstraint.getColumns().get(0).setUnique(true);
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292 }
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
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);
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
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 }
373
374
375
376
377
378 }
379
380
381
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 }