001: /*
002: * Copyright 2003 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package velosurf.sql;
018:
019: import java.util.Map;
020: import java.util.HashMap;
021: import java.util.List;
022: import java.util.ArrayList;
023: import java.sql.SQLException;
024: import java.sql.DatabaseMetaData;
025: import java.sql.ResultSet;
026:
027: import velosurf.model.Entity;
028: import velosurf.model.ImportedKey;
029: import velosurf.model.ExportedKey;
030: import velosurf.util.Logger;
031: import velosurf.util.StringLists;
032:
033: /**
034: * Class used to reverse engine a database.
035: */
036: public class ReverseEngineer {
037:
038: /** map table->entity, valid only between readConfigFile and readMetaData.
039: */
040: private Map<String, String> entityByTableName = new HashMap<String, String>();
041: /** Database. */
042: private Database db;
043: /** Driver infos. */
044: private DriverInfo driverInfo;
045: /** Reverse mode. */
046: private int reverseMode = DEFAULT_REVERSE_MODE;
047: /** No reverse engineering mode. */
048: public static final int REVERSE_NONE = 0;
049: /** Partial reverse engineering mode. */
050: public static final int REVERSE_PARTIAL = 1;
051: /** Tables reverse engineering mode. */
052: public static final int REVERSE_TABLES = 2;
053: /** Full reverse engineering mode. */
054: public static final int REVERSE_FULL = 3;
055: /** Default reverse engineering mode. */
056: public static final int DEFAULT_REVERSE_MODE = REVERSE_FULL;
057: /** Reverse engineering mode names. */
058: public static String[] reverseModeName = { "none", "partial",
059: "tables", "full" };
060:
061: /** constructor.
062: *
063: * @param database database
064: */
065: public ReverseEngineer(Database database) {
066: db = database;
067: }
068:
069: /** Driver infos setter.
070: *
071: * @param di driver infos
072: */
073: protected void setDriverInfo(DriverInfo di) {
074: driverInfo = di;
075: }
076:
077: /**
078: * Set reverse mode.
079: * @param reverseMethod reverse mode
080: */
081: protected void setReverseMode(int reverseMethod) {
082: reverseMode = reverseMethod;
083: }
084:
085: /**
086: * add a table <-> entity matching.
087: * @param tableName
088: * @param entity
089: */
090: protected void addTableMatching(String tableName, Entity entity) {
091: if (db.getSchema() != null) {
092: tableName = db.getSchema() + "." + tableName;
093: }
094: entityByTableName.put(tableName, entity.getName());
095: }
096:
097: /** read the meta data from the database.
098: *
099: * @exception java.sql.SQLException thrown by the database engine
100: */
101: protected void readMetaData() throws SQLException {
102:
103: DatabaseMetaData meta = db.getConnection().getMetaData();
104: ResultSet tables = null;
105:
106: // extra debug information about which jdbc driver we are using
107: Logger.info("Database Product Name: "
108: + meta.getDatabaseProductName());
109: Logger.info("Database Product Version: "
110: + meta.getDatabaseProductVersion());
111: Logger
112: .info("JDBC Driver Name: "
113: + meta.getDriverName());
114: Logger.info("JDBC Driver Version: "
115: + meta.getDriverVersion());
116:
117: /* columns and primary keys */
118: try {
119: Logger.debug("reverse enginering: mode = "
120: + reverseModeName[reverseMode]);
121: if (reverseMode == REVERSE_NONE) {
122: return;
123: }
124:
125: switch (reverseMode) {
126: case REVERSE_TABLES:
127: case REVERSE_FULL:
128: break;
129: case REVERSE_PARTIAL:
130: break;
131: }
132:
133: switch (reverseMode) {
134: case REVERSE_TABLES:
135: case REVERSE_FULL:
136: tables = meta.getTables(null, db.getSchema(), null,
137: null);
138: while (tables.next()) {
139: String tableName = adaptCase(tables
140: .getString("TABLE_NAME"));
141: if (driverInfo.ignoreTable(tableName)) {
142: continue;
143: }
144: if (db.getSchema() != null) {
145: tableName = db.getSchema() + tableName;
146: }
147: String entityName = entityByTableName
148: .get(tableName);
149: Entity entity = null;
150: if (entityName != null) {
151: entity = db.getEntity(entityName);
152: }
153: if (entity == null) {
154: entity = db.getEntityCreate(tableName);
155: entityByTableName.put(tableName, entity
156: .getName());
157: }
158: readTableMetaData(meta, entity, tableName);
159: }
160: /* not any more valid since entityByTableName does now keep mappings TODO alternative
161: for(Iterator e = entityByTableName.keySet().iterator();e.hasNext();)
162: {
163: Logger.warn("table '"+(String)e.next()+"' not found!");
164: }
165: */
166: break;
167: case REVERSE_PARTIAL:
168: for (Entity entity : db.getEntities().values()) {
169: String tableName = entity.getTableName();
170: readTableMetaData(meta, entity, tableName);
171: }
172: break;
173: case REVERSE_NONE:
174: break;
175: }
176: } finally {
177: if (tables != null)
178: tables.close();
179: }
180:
181: /* imported and exported keys */
182: if (reverseMode == REVERSE_FULL) {
183: try {
184: tables = meta.getTables(null, db.getSchema(), null,
185: null);
186: while (tables.next()) {
187: String tableName = adaptCase(tables
188: .getString("TABLE_NAME"));
189: if (driverInfo.ignoreTable(tableName)) {
190: continue;
191: }
192: if (db.getSchema() != null) {
193: tableName = db.getSchema() + tableName;
194: }
195: String entityName = entityByTableName
196: .get(tableName);
197: Entity entity = db.getEntity(entityName);
198:
199: readForeignKeys(meta, entity, tableName);
200: }
201: } finally {
202: if (tables != null)
203: tables.close();
204: }
205: }
206:
207: }
208:
209: /** Read table meta data.
210: *
211: * @param meta database meta data
212: * @param entity corresponding entity
213: * @param tableName table name
214: * @throws SQLException
215: */
216: private void readTableMetaData(DatabaseMetaData meta,
217: Entity entity, String tableName) throws SQLException {
218:
219: List<String> keylist = StringLists.getPopulatedArrayList(10); // ever seen a primary key with more than 10 columns ?!
220:
221: /* read columns */
222: ResultSet cols = null;
223: try {
224: cols = meta.getColumns(null, db.getSchema(), tableName,
225: null);
226: while (cols.next()) {
227: String column = adaptCase(cols.getString("COLUMN_NAME"));
228: entity.addColumn(column, cols.getInt("DATA_TYPE"));
229: }
230: } finally {
231: if (cols != null)
232: cols.close();
233: }
234:
235: /* read primary keys */
236: ResultSet pks = null;
237: try {
238: pks = meta.getPrimaryKeys(null, db.getSchema(), tableName);
239: int keysize = 0;
240: while (pks.next()) {
241: short ord = pks.getShort("KEY_SEQ");
242: String column = adaptCase(pks.getString("COLUMN_NAME"));
243: keylist.set(ord - 1, column);
244: keysize++;
245: }
246: for (int k = 0; k < keysize; k++) {
247: entity.addPKColumn((String) keylist.get(k));
248: }
249: } finally {
250: if (pks != null)
251: pks.close();
252: }
253:
254: entity.reverseEnginered();
255: }
256:
257: /**
258: * Read foreign keys.
259: * @param meta database meta data
260: * @param entity entity
261: * @param tableName table name
262: * @throws SQLException
263: */
264: private void readForeignKeys(DatabaseMetaData meta, Entity entity,
265: String tableName) throws SQLException {
266: /* read imported keys */
267: if (reverseMode == REVERSE_FULL) {
268: ResultSet iks = null;
269: try {
270: iks = meta.getImportedKeys(null, db.getSchema(),
271: tableName);
272: short ord;
273: String pkSchema = null, pkTable = null;
274: List<String> fkCols = new ArrayList<String>();
275: List<String> pkCols = new ArrayList<String>();
276: while (iks.next()) {
277: ord = iks.getShort("KEY_SEQ");
278: if (ord == 1) {
279: /* save previous key */
280: if (fkCols.size() > 0) {
281: addImportedKey(entity, pkSchema, pkTable,
282: pkCols, fkCols);
283: fkCols.clear();
284: pkCols.clear();
285: }
286: pkSchema = adaptCase(iks
287: .getString("PKTABLE_SCHEM"));
288: pkTable = adaptCase(iks
289: .getString("PKTABLE_NAME"));
290: }
291: pkCols
292: .add(adaptCase(iks
293: .getString("PKCOLUMN_NAME")));
294: fkCols
295: .add(adaptCase(iks
296: .getString("FKCOLUMN_NAME")));
297: }
298: /* save last key */
299: if (pkCols.size() > 0) {
300: addImportedKey(entity, pkSchema, pkTable, pkCols,
301: fkCols);
302: }
303: } finally {
304: if (iks != null)
305: iks.close();
306: }
307: }
308:
309: /* read exported keys */
310: if (reverseMode == REVERSE_FULL) {
311: ResultSet eks = null;
312: try {
313: eks = meta.getExportedKeys(null, db.getSchema(),
314: tableName);
315: short ord;
316: String fkSchema = null, fkTable = null;
317: List<String> fkCols = new ArrayList<String>();
318: List<String> pkCols = new ArrayList<String>();
319: while (eks.next()) {
320: ord = eks.getShort("KEY_SEQ");
321: if (ord == 1) {
322: /* save previous key */
323: if (fkCols.size() > 0) {
324: addExportedKey(entity, fkSchema, fkTable,
325: fkCols, pkCols);
326: fkCols.clear();
327: pkCols.clear();
328: }
329: fkSchema = adaptCase(eks
330: .getString("FKTABLE_SCHEM"));
331: fkTable = adaptCase(eks
332: .getString("FKTABLE_NAME"));
333: }
334: pkCols
335: .add(adaptCase(eks
336: .getString("PKCOLUMN_NAME")));
337: fkCols
338: .add(adaptCase(eks
339: .getString("FKCOLUMN_NAME")));
340: }
341: /* save last key */
342: if (pkCols.size() > 0) {
343: addExportedKey(entity, fkSchema, fkTable, fkCols,
344: pkCols);
345: }
346: } finally {
347: if (eks != null)
348: eks.close();
349: }
350: }
351: }
352:
353: /**
354: * Add a new exported key.
355: * @param entity target entity
356: * @param fkSchema foreign key schema
357: * @param fkTable foreign key table
358: * @param fkCols foreign key columns
359: * @param pkCols primary key columns
360: */
361: private void addExportedKey(Entity entity, String fkSchema,
362: String fkTable, List<String> fkCols, List<String> pkCols) {
363: String fkEntityName = getEntityByTable(fkSchema, fkTable);
364: if (fkEntityName == null) {
365: Logger.warn("reverse: ignoring exported key " + fkSchema
366: + "." + fkTable
367: + ": corresponding entity not found.");
368: } else {
369: if (fkSchema != null) {
370: fkTable = fkSchema + "." + fkTable;
371: }
372: Entity fkEntity = db.getEntity(fkEntityName);
373: fkCols = sortColumns(entity.getPKCols(), pkCols, fkCols);
374: // Logger.trace("reverse: found exported key: "+entity.getName()+"("+StringLists.join(pkCols,",")+") <- "+fkTable+"("+StringLists.join(fkCols,",")+")");
375: ExportedKey definedKey = entity.findExportedKey(fkEntity,
376: fkCols);
377: if (definedKey == null) {
378: entity.addAttribute(new ExportedKey(
379: getExportedKeyName(fkEntityName), entity,
380: fkTable, new ArrayList<String>(fkCols)));
381: } else if (definedKey.getFKCols() == null) {
382: definedKey.setFKCols(fkCols);
383: }
384: }
385: }
386:
387: /**
388: * Add a new imported key.
389: * @param entity target entity
390: * @param pkSchema primary key schema
391: * @param pkTable primary key table
392: * @param pkCols primary key columns
393: * @param fkCols foreign key columns
394: */
395: private void addImportedKey(Entity entity, String pkSchema,
396: String pkTable, List<String> pkCols, List<String> fkCols) {
397: String pkEntityName = getEntityByTable(pkSchema, pkTable);
398: if (pkEntityName == null) {
399: Logger.warn("reverse: ignoring imported key " + pkSchema
400: + "." + pkTable
401: + ": corresponding entity not found.");
402: } else {
403: if (pkSchema != null) {
404: pkTable = pkSchema + "." + pkTable;
405: }
406: Entity pkEntity = db.getEntity(pkEntityName);
407: fkCols = sortColumns(pkEntity.getPKCols(), pkCols, fkCols);
408: // Logger.trace("reverse: found imported key: "+entity.getName()+"("+StringLists.join(fkCols,",")+") -> "+pkTable+"("+StringLists.join(pkCols,",")+")");
409: ImportedKey definedKey = entity.findImportedKey(pkEntity,
410: fkCols);
411: if (definedKey == null) {
412: entity.addAttribute(new ImportedKey(pkEntityName,
413: entity, pkEntityName, new ArrayList<String>(
414: fkCols)));
415: } else if (definedKey.getFKCols() == null) {
416: definedKey.setFKCols(fkCols);
417: }
418: }
419: }
420:
421: /** Get an entity by its table name.
422: *
423: * @param schema schema name
424: * @param table table name
425: * @return entity name
426: */
427: private String getEntityByTable(String schema, String table) {
428: String entityName;
429: if (schema == null) {
430: if (db.getSchema() == null) {
431: entityName = entityByTableName.get(table);
432: } else {
433: /* buggy jdbc driver */
434: entityName = entityByTableName.get(db.getSchema() + "."
435: + table);
436: }
437: } else {
438: if (db.getSchema() == null) {
439: /* means the jdbc driver is using a default schema name,
440: * like PUBLIC for hsqldb (OR that we want an instance in another database connexion. TODO) */
441: entityName = entityByTableName
442: .get(schema + "." + table);
443: if (entityName == null) {
444: entityName = entityByTableName.get(table);
445: }
446: } else {
447: /* what if not equal? rather strange... */
448: if (schema.equals(db.getSchema())) {
449: Logger.error("reverse: was expecting schema '"
450: + db.getSchema() + "' and got schema '"
451: + schema + "'! Please report this error.");
452: }
453: entityName = entityByTableName
454: .get(schema + "." + table);
455: }
456: }
457: return entityName;
458: }
459:
460: /** Sort columns in <code>target</code> the same way <code>unordered</code> would have to
461: * be sorted to be like <code>ordered</code>.
462: * @param ordered ordered list reference
463: * @param unordered unordered list reference
464: * @param target target list
465: * @return sorted target list
466: */
467: private List<String> sortColumns(List<String> ordered,
468: List<String> unordered, List<String> target) {
469: if (ordered.size() == 1) {
470: return target;
471: }
472: List<String> sorted = new ArrayList<String>();
473: for (String col : ordered) {
474: int i = unordered.indexOf(col);
475: assert (i != -1);
476: sorted.add(target.get(i));
477: }
478: return sorted;
479: }
480:
481: /** adapt case
482: *
483: * @param name name to adapt
484: * @return adapted name
485: */
486: private String adaptCase(String name) {
487: return db.adaptCase(name);
488: }
489:
490: /**
491: * rough plural calculation.
492: * @param name singular
493: * @return plural
494: */
495: private String getExportedKeyName(String name) {
496: return adaptCase(name.endsWith("s") ? name + "es" : name + "s");
497: }
498: }
|