package org.springframework.roo.addon.dbre.addon.model; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; /** * Creates a {@link Database database} model from a live database using JDBC. * * @author Alan Stewart * @since 1.1 */ public class DatabaseIntrospector extends AbstractIntrospector { private final Set<String> excludeTables; private final Set<String> includeTables; private final Set<Schema> schemas; private final boolean view; public DatabaseIntrospector(final Connection connection, final Set<Schema> schemas, final boolean view, final Set<String> includeTables, final Set<String> excludeTables) throws SQLException { super(connection); this.schemas = schemas; this.view = view; this.includeTables = includeTables; this.excludeTables = excludeTables; } public Database createDatabase() throws SQLException { final Set<Table> tables = new LinkedHashSet<Table>(); for (final Schema schema : schemas) { tables.addAll(getTables(schema)); } return new Database(tables); } private Index findIndex(final String name, final Set<Index> indices) { for (final Index index : indices) { if (index.getName().equalsIgnoreCase(name)) { return index; } } return null; } private String getArtifact(final String artifactName) throws SQLException { if (databaseMetaData.storesLowerCaseIdentifiers()) { return StringUtils.lowerCase(artifactName); } else if (databaseMetaData.storesUpperCaseIdentifiers()) { return StringUtils.upperCase(artifactName); } else { return artifactName; } } private CascadeAction getCascadeAction(final Short actionValue) { CascadeAction cascadeAction; switch (actionValue.intValue()) { case DatabaseMetaData.importedKeyCascade: cascadeAction = CascadeAction.CASCADE; break; case DatabaseMetaData.importedKeySetNull: cascadeAction = CascadeAction.SET_NULL; break; case DatabaseMetaData.importedKeySetDefault: cascadeAction = CascadeAction.SET_DEFAULT; break; case DatabaseMetaData.importedKeyRestrict: cascadeAction = CascadeAction.RESTRICT; break; case DatabaseMetaData.importedKeyNoAction: cascadeAction = CascadeAction.NONE; break; default: cascadeAction = CascadeAction.NONE; } return cascadeAction; } private Set<Table> getTables(final Schema schema) throws SQLException { final Set<Table> tables = new LinkedHashSet<Table>(); final String[] types = view ? new String[] {TableType.TABLE.name(), TableType.VIEW.name()} : new String[] {TableType.TABLE.name()}; final ResultSet rs = databaseMetaData.getTables(null, getArtifact(schema.getName()), null, types); try { while (rs.next()) { final String tableName = rs.getString("TABLE_NAME"); // Check for certain tables such as Oracle recycle bin tables, // and ignore if (ignoreTables(tableName)) { continue; } if (hasIncludedTable(tableName) && !hasExcludedTable(tableName)) { final Table table = new Table(tableName, new Schema(rs.getString("TABLE_SCHEM"))); table.setCatalog(rs.getString("TABLE_CAT")); table.setDescription(rs.getString("REMARKS")); readColumns(table); readForeignKeys(table, false); readForeignKeys(table, true); readIndices(table); for (final String columnName : readPrimaryKeyNames(table)) { final Column column = table.findColumn(columnName); if (column != null) { column.setPrimaryKey(true); } } tables.add(table); } } } finally { rs.close(); } return tables; } private boolean hasExcludedTable(final String tableName) { if (excludeTables == null || excludeTables.isEmpty()) { return false; } return hasTable(excludeTables, tableName); } private boolean hasIncludedTable(final String tableName) { if (includeTables == null || includeTables.isEmpty()) { return true; } return hasTable(includeTables, tableName); } private boolean hasTable(final Set<String> tables, final String tableName) { for (final String table : tables) { final String regex = table.replaceAll("\\*", ".*").replaceAll("\\?", ".?"); final Pattern pattern = Pattern.compile(regex); if (pattern.matcher(tableName).matches()) { return true; } } return false; } private boolean ignoreTables(final String tableName) { boolean ignore = false; try { if ("Oracle".equalsIgnoreCase(databaseMetaData.getDatabaseProductName()) && tableName.startsWith("BIN$")) { ignore = true; } if ("MySQL".equalsIgnoreCase(databaseMetaData.getDatabaseProductName()) && tableName.equals("SEQUENCE")) { ignore = true; } } catch (final SQLException ignored) { } return ignore; } private void readColumns(final Table table) throws SQLException { final ResultSet rs = databaseMetaData.getColumns(table.getCatalog(), table.getSchema().getName(), table.getName(), null); try { while (rs.next()) { final Column column = new Column(rs.getString("COLUMN_NAME"), rs.getInt("DATA_TYPE"), rs.getString("TYPE_NAME"), rs.getInt("COLUMN_SIZE"), rs.getInt("DECIMAL_DIGITS")); column.setDescription(rs.getString("REMARKS")); column.setDefaultValue(rs.getString("COLUMN_DEF")); column.setRequired("NO".equalsIgnoreCase(rs.getString("IS_NULLABLE"))); table.addColumn(column); } } finally { rs.close(); } } private void readForeignKeys(final Table table, final boolean exported) throws SQLException { final Map<String, ForeignKey> foreignKeys = new LinkedHashMap<String, ForeignKey>(); ResultSet rs; if (exported) { rs = databaseMetaData.getExportedKeys(table.getCatalog(), table.getSchema().getName(), table.getName()); } else { rs = databaseMetaData.getImportedKeys(table.getCatalog(), table.getSchema().getName(), table.getName()); } try { while (rs.next()) { final String name = rs.getString("FK_NAME"); final String foreignTableName = rs.getString(exported ? "FKTABLE_NAME" : "PKTABLE_NAME"); final String key = name + "_" + foreignTableName; if (!hasExcludedTable(foreignTableName)) { final ForeignKey foreignKey = new ForeignKey(name, foreignTableName); foreignKey.setForeignSchemaName(StringUtils.defaultIfEmpty( rs.getString(exported ? "FKTABLE_SCHEM" : "PKTABLE_SCHEM"), DbreModelService.NO_SCHEMA_REQUIRED)); foreignKey.setOnUpdate(getCascadeAction(rs.getShort("UPDATE_RULE"))); foreignKey.setOnDelete(getCascadeAction(rs.getShort("DELETE_RULE"))); foreignKey.setExported(exported); final String localColumnName = rs.getString(exported ? "PKCOLUMN_NAME" : "FKCOLUMN_NAME"); final String foreignColumnName = rs.getString(exported ? "FKCOLUMN_NAME" : "PKCOLUMN_NAME"); final Reference reference = new Reference(localColumnName, foreignColumnName); if (foreignKeys.containsKey(key)) { foreignKeys.get(key).addReference(reference); } else { foreignKey.addReference(reference); foreignKeys.put(key, foreignKey); } } } } finally { rs.close(); } for (final ForeignKey foreignKey : foreignKeys.values()) { if (exported) { table.addExportedKey(foreignKey); } else { table.addImportedKey(foreignKey); } } } private void readIndices(final Table table) throws SQLException { final Set<Index> indices = new LinkedHashSet<Index>(); ResultSet rs; try { // Catching SQLException here due to Oracle throwing exception when // attempting to retrieve indices for deleted tables that exist in // Oracle's recycle bin rs = databaseMetaData.getIndexInfo(table.getCatalog(), table.getSchema().getName(), table.getName(), false, false); } catch (final SQLException e) { return; } if (rs != null) { try { while (rs.next()) { final Short type = rs.getShort("TYPE"); if (type == DatabaseMetaData.tableIndexStatistic) { continue; } final String indexName = rs.getString("INDEX_NAME"); Index index = findIndex(indexName, indices); if (index == null) { index = new Index(indexName); } else { indices.remove(index); } index.setUnique(!rs.getBoolean("NON_UNIQUE")); final IndexColumn indexColumn = new IndexColumn(rs.getString("COLUMN_NAME")); index.addColumn(indexColumn); indices.add(index); } } finally { rs.close(); } } for (final Index index : indices) { table.addIndex(index); } } private Set<String> readPrimaryKeyNames(final Table table) throws SQLException { final Set<String> columnNames = new LinkedHashSet<String>(); final ResultSet rs = databaseMetaData.getPrimaryKeys(table.getCatalog(), table.getSchema().getName(), table.getName()); try { while (rs.next()) { columnNames.add(rs.getString("COLUMN_NAME")); } } finally { rs.close(); } return columnNames; } }