blob: a21e8a83a8414508ce41f7a256ae0d99cdb5d731 [file] [log] [blame]
package org.apache.ddlutils.platform;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.map.ListOrderedMap;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ddlutils.Platform;
import org.apache.ddlutils.PlatformInfo;
import org.apache.ddlutils.model.CascadeActionEnum;
import org.apache.ddlutils.model.Column;
import org.apache.ddlutils.model.Database;
import org.apache.ddlutils.model.ForeignKey;
import org.apache.ddlutils.model.Index;
import org.apache.ddlutils.model.IndexColumn;
import org.apache.ddlutils.model.NonUniqueIndex;
import org.apache.ddlutils.model.Reference;
import org.apache.ddlutils.model.Table;
import org.apache.ddlutils.model.UniqueIndex;
/**
* An utility class to create a Database model from a live database.
*
* @version $Revision$
*/
public class JdbcModelReader
{
/** The Log to which logging calls will be made. */
private final Log _log = LogFactory.getLog(getClass());
/** The descriptors for the relevant columns in the table meta data. */
private final List _columnsForTable;
/** The descriptors for the relevant columns in the table column meta data. */
private final List _columnsForColumn;
/** The descriptors for the relevant columns in the primary key meta data. */
private final List _columnsForPK;
/** The descriptors for the relevant columns in the foreign key meta data. */
private final List _columnsForFK;
/** The descriptors for the relevant columns in the index meta data. */
private final List _columnsForIndex;
/** The platform that this model reader belongs to. */
private Platform _platform;
/** Contains default column sizes (minimum sizes that a JDBC-compliant db must support). */
private HashMap _defaultSizes = new HashMap();
/** The default database catalog to read. */
private String _defaultCatalogPattern = "%";
/** The default database schema(s) to read. */
private String _defaultSchemaPattern = "%";
/** The default pattern for reading all tables. */
private String _defaultTablePattern = "%";
/** The default pattern for reading all columns. */
private String _defaultColumnPattern;
/** The table types to recognize per default. */
private String[] _defaultTableTypes = { "TABLE" };
/** The active connection while reading a database model. */
private Connection _connection;
/**
* Creates a new model reader instance.
*
* @param platform The plaftform this builder belongs to
*/
public JdbcModelReader(Platform platform)
{
_platform = platform;
_defaultSizes.put(new Integer(Types.CHAR), "254");
_defaultSizes.put(new Integer(Types.VARCHAR), "254");
_defaultSizes.put(new Integer(Types.LONGVARCHAR), "254");
_defaultSizes.put(new Integer(Types.BINARY), "254");
_defaultSizes.put(new Integer(Types.VARBINARY), "254");
_defaultSizes.put(new Integer(Types.LONGVARBINARY), "254");
_defaultSizes.put(new Integer(Types.INTEGER), "32");
_defaultSizes.put(new Integer(Types.BIGINT), "64");
_defaultSizes.put(new Integer(Types.REAL), "7,0");
_defaultSizes.put(new Integer(Types.FLOAT), "15,0");
_defaultSizes.put(new Integer(Types.DOUBLE), "15,0");
_defaultSizes.put(new Integer(Types.DECIMAL), "15,15");
_defaultSizes.put(new Integer(Types.NUMERIC), "15,15");
_columnsForTable = initColumnsForTable();
_columnsForColumn = initColumnsForColumn();
_columnsForPK = initColumnsForPK();
_columnsForFK = initColumnsForFK();
_columnsForIndex = initColumnsForIndex();
}
/**
* Returns the log used by the model reader.
*
* @return The log
*/
protected Log getLog()
{
return _log;
}
/**
* Returns the platform that this model reader belongs to.
*
* @return The platform
*/
public Platform getPlatform()
{
return _platform;
}
/**
* Returns the platform specific settings.
*
* @return The platform settings
*/
public PlatformInfo getPlatformInfo()
{
return _platform.getPlatformInfo();
}
/**
* Returns descriptors for the columns that shall be read from the result set when
* reading the meta data for a table. Note that the columns are read in the order
* defined by this list.<br/>
* Redefine this method if you want more columns or a different order.
*
* @return The descriptors for the result set columns
*/
protected List initColumnsForTable()
{
List result = new ArrayList();
result.add(new MetaDataColumnDescriptor("TABLE_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("TABLE_TYPE", Types.VARCHAR, "UNKNOWN"));
result.add(new MetaDataColumnDescriptor("TABLE_CAT", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("TABLE_SCHEM", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("REMARKS", Types.VARCHAR));
return result;
}
/**
* Returns descriptors for the columns that shall be read from the result set when
* reading the meta data for table columns. Note that the columns are read in the order
* defined by this list.<br/>
* Redefine this method if you want more columns or a different order.
*
* @return The map column name -> descriptor for the result set columns
*/
protected List initColumnsForColumn()
{
List result = new ArrayList();
// As suggested by Alexandre Borgoltz, we're reading the COLUMN_DEF first because Oracle
// has problems otherwise (it seemingly requires a LONG column to be the first to be read)
// See also DDLUTILS-29
result.add(new MetaDataColumnDescriptor("COLUMN_DEF", Types.VARCHAR));
// we're also reading the table name so that a model reader impl can filter manually
result.add(new MetaDataColumnDescriptor("TABLE_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("COLUMN_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("DATA_TYPE", Types.INTEGER, new Integer(java.sql.Types.OTHER)));
result.add(new MetaDataColumnDescriptor("NUM_PREC_RADIX", Types.INTEGER, new Integer(10)));
result.add(new MetaDataColumnDescriptor("DECIMAL_DIGITS", Types.INTEGER, new Integer(0)));
result.add(new MetaDataColumnDescriptor("COLUMN_SIZE", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("IS_NULLABLE", Types.VARCHAR, "YES"));
result.add(new MetaDataColumnDescriptor("REMARKS", Types.VARCHAR));
return result;
}
/**
* Returns descriptors for the columns that shall be read from the result set when
* reading the meta data for primary keys. Note that the columns are read in the order
* defined by this list.<br/>
* Redefine this method if you want more columns or a different order.
*
* @return The map column name -> descriptor for the result set columns
*/
protected List initColumnsForPK()
{
List result = new ArrayList();
result.add(new MetaDataColumnDescriptor("COLUMN_NAME", Types.VARCHAR));
// we're also reading the table name so that a model reader impl can filter manually
result.add(new MetaDataColumnDescriptor("TABLE_NAME", Types.VARCHAR));
// the name of the primary key is currently only interesting to the pk index name resolution
result.add(new MetaDataColumnDescriptor("PK_NAME", Types.VARCHAR));
return result;
}
/**
* Returns descriptors for the columns that shall be read from the result set when
* reading the meta data for foreign keys originating from a table. Note that the
* columns are read in the order defined by this list.<br/>
* Redefine this method if you want more columns or a different order.
*
* @return The map column name -> descriptor for the result set columns
*/
protected List initColumnsForFK()
{
List result = new ArrayList();
result.add(new MetaDataColumnDescriptor("PKTABLE_NAME", Types.VARCHAR));
// we're also reading the table name so that a model reader impl can filter manually
result.add(new MetaDataColumnDescriptor("FKTABLE_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("KEY_SEQ", Types.TINYINT, new Short((short)0)));
result.add(new MetaDataColumnDescriptor("FK_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("UPDATE_RULE", Types.TINYINT));
result.add(new MetaDataColumnDescriptor("DELETE_RULE", Types.TINYINT));
result.add(new MetaDataColumnDescriptor("PKCOLUMN_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("FKCOLUMN_NAME", Types.VARCHAR));
return result;
}
/**
* Returns descriptors for the columns that shall be read from the result set when
* reading the meta data for indices. Note that the columns are read in the order
* defined by this list.<br/>
* Redefine this method if you want more columns or a different order.
*
* @return The map column name -> descriptor for the result set columns
*/
protected List initColumnsForIndex()
{
List result = new ArrayList();
result.add(new MetaDataColumnDescriptor("INDEX_NAME", Types.VARCHAR));
// we're also reading the table name so that a model reader impl can filter manually
result.add(new MetaDataColumnDescriptor("TABLE_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("NON_UNIQUE", Types.BIT, Boolean.TRUE));
result.add(new MetaDataColumnDescriptor("ORDINAL_POSITION", Types.TINYINT, new Short((short)0)));
result.add(new MetaDataColumnDescriptor("COLUMN_NAME", Types.VARCHAR));
result.add(new MetaDataColumnDescriptor("TYPE", Types.TINYINT));
return result;
}
/**
* Returns the catalog(s) in the database to read per default.
*
* @return The default catalog(s)
*/
public String getDefaultCatalogPattern()
{
return _defaultCatalogPattern;
}
/**
* Sets the catalog(s) in the database to read per default.
*
* @param catalogPattern The catalog(s)
*/
public void setDefaultCatalogPattern(String catalogPattern)
{
_defaultCatalogPattern = catalogPattern;
}
/**
* Returns the schema(s) in the database to read per default.
*
* @return The default schema(s)
*/
public String getDefaultSchemaPattern()
{
return _defaultSchemaPattern;
}
/**
* Sets the schema(s) in the database to read per default.
*
* @param schemaPattern The schema(s)
*/
public void setDefaultSchemaPattern(String schemaPattern)
{
_defaultSchemaPattern = schemaPattern;
}
/**
* Returns the default pattern to read the relevant tables from the database.
*
* @return The table pattern
*/
public String getDefaultTablePattern()
{
return _defaultTablePattern;
}
/**
* Sets the default pattern to read the relevant tables from the database.
*
* @param tablePattern The table pattern
*/
public void setDefaultTablePattern(String tablePattern)
{
_defaultTablePattern = tablePattern;
}
/**
* Returns the default pattern to read the relevant columns from the database.
*
* @return The column pattern
*/
public String getDefaultColumnPattern()
{
return _defaultColumnPattern;
}
/**
* Sets the default pattern to read the relevant columns from the database.
*
* @param columnPattern The column pattern
*/
public void setDefaultColumnPattern(String columnPattern)
{
_defaultColumnPattern = columnPattern;
}
/**
* Returns the table types to recognize per default.
*
* @return The default table types
*/
public String[] getDefaultTableTypes()
{
if (_defaultTableTypes == null)
{
return null;
}
else
{
String[] result = new String[_defaultTableTypes.length];
System.arraycopy(_defaultTableTypes, 0, result, 0, _defaultTableTypes.length);
return result;
}
}
/**
* Sets the table types to recognize per default. Typical types are "TABLE", "VIEW",
* "SYSTEM TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM".
*
* @param types The table types
*/
public void setDefaultTableTypes(String[] types)
{
if (types == null)
{
_defaultTableTypes = null;
}
else
{
_defaultTableTypes = new String[types.length];
System.arraycopy(types, 0, types, 0, types.length);
}
}
/**
* Returns the descriptors for the columns to be read from the table meta data result set.
*
* @return The column descriptors
*/
protected List getColumnsForTable()
{
return _columnsForTable;
}
/**
* Returns the descriptors for the columns to be read from the column meta data result set.
*
* @return The column descriptors
*/
protected List getColumnsForColumn()
{
return _columnsForColumn;
}
/**
* Returns the descriptors for the columns to be read from the primary key meta data result set.
*
* @return The column descriptors
*/
protected List getColumnsForPK()
{
return _columnsForPK;
}
/**
* Returns the descriptors for the columns to be read from the foreign key meta data result set.
*
* @return The column descriptors
*/
protected List getColumnsForFK()
{
return _columnsForFK;
}
/**
* Returns the descriptors for the columns to be read from the index meta data result set.
*
* @return The column descriptors
*/
protected List getColumnsForIndex()
{
return _columnsForIndex;
}
/**
* Returns the active connection. Note that this is only set during a call to
* {@link #readTables(String, String, String[])}.
*
* @return The connection or <code>null</code> if there is no active connection
*/
protected Connection getConnection()
{
return _connection;
}
/**
* Reads the database model from the given connection.
*
* @param connection The connection
* @param name The name of the resulting database; <code>null</code> when the default name (the catalog)
* is desired which might be <code>null</code> itself though
* @return The database model
*/
public Database getDatabase(Connection connection, String name) throws SQLException
{
return getDatabase(connection, name, null, null, null);
}
/**
* Reads the database model from the given connection.
*
* @param connection The connection
* @param name The name of the resulting database; <code>null</code> when the default name (the catalog)
* is desired which might be <code>null</code> itself though
* @param catalog The catalog to acess in the database; use <code>null</code> for the default value
* @param schema The schema to acess in the database; use <code>null</code> for the default value
* @param tableTypes The table types to process; use <code>null</code> or an empty list for the default ones
* @return The database model
*/
public Database getDatabase(Connection connection, String name, String catalog, String schema, String[] tableTypes) throws SQLException
{
Database db = new Database();
if (name == null)
{
try
{
db.setName(connection.getCatalog());
if (catalog == null)
{
catalog = db.getName();
}
}
catch (Exception ex)
{
_log.info("Cannot determine the catalog name from connection.", ex);
}
}
else
{
db.setName(name);
}
try
{
_connection = connection;
db.addTables(readTables(catalog, schema, tableTypes));
// Note that we do this here instead of in readTable since platforms may redefine the
// readTable method whereas it is highly unlikely that this method gets redefined
if (getPlatform().isForeignKeysSorted())
{
sortForeignKeys(db);
}
}
finally
{
_connection = null;
}
db.initialize();
return db;
}
/**
* Reads the tables from the database metadata.
*
* @param catalog The catalog to acess in the database; use <code>null</code> for the default value
* @param schemaPattern The schema(s) to acess in the database; use <code>null</code> for the default value
* @param tableTypes The table types to process; use <code>null</code> or an empty list for the default ones
* @return The tables
*/
protected Collection readTables(String catalog, String schemaPattern, String[] tableTypes) throws SQLException
{
ResultSet tableData = null;
try
{
DatabaseMetaDataWrapper metaData = new DatabaseMetaDataWrapper();
metaData.setMetaData(_connection.getMetaData());
metaData.setCatalog(catalog == null ? getDefaultCatalogPattern() : catalog);
metaData.setSchemaPattern(schemaPattern == null ? getDefaultSchemaPattern() : schemaPattern);
metaData.setTableTypes((tableTypes == null) || (tableTypes.length == 0) ? getDefaultTableTypes() : tableTypes);
tableData = metaData.getTables(getDefaultTablePattern());
List tables = new ArrayList();
while (tableData.next())
{
Map values = readColumns(tableData, getColumnsForTable());
Table table = readTable(metaData, values);
if (table != null)
{
tables.add(table);
}
}
final Collator collator = Collator.getInstance();
Collections.sort(tables, new Comparator() {
public int compare(Object obj1, Object obj2)
{
return collator.compare(((Table)obj1).getName().toUpperCase(), ((Table)obj2).getName().toUpperCase());
}
});
return tables;
}
finally
{
closeResultSet(tableData);
}
}
/**
* Reads the next table from the meta data.
*
* @param metaData The database meta data
* @param values The table metadata values as defined by {@link #getColumnsForTable()}
* @return The table or <code>null</code> if the result set row did not contain a valid table
*/
protected Table readTable(DatabaseMetaDataWrapper metaData, Map values) throws SQLException
{
String tableName = (String)values.get("TABLE_NAME");
Table table = null;
if ((tableName != null) && (tableName.length() > 0))
{
table = new Table();
table.setName(tableName);
table.setType((String)values.get("TABLE_TYPE"));
table.setCatalog((String)values.get("TABLE_CAT"));
table.setSchema((String)values.get("TABLE_SCHEM"));
table.setDescription((String)values.get("REMARKS"));
table.addColumns(readColumns(metaData, tableName));
table.addForeignKeys(readForeignKeys(metaData, tableName));
table.addIndices(readIndices(metaData, tableName));
Collection primaryKeys = readPrimaryKeyNames(metaData, tableName);
for (Iterator it = primaryKeys.iterator(); it.hasNext();)
{
table.findColumn((String)it.next(), true).setPrimaryKey(true);
}
if (getPlatformInfo().isSystemIndicesReturned())
{
removeSystemIndices(metaData, table);
}
}
return table;
}
/**
* Removes system indices (generated by the database for primary and foreign keys)
* from the table.
*
* @param metaData The database meta data
* @param table The table
*/
protected void removeSystemIndices(DatabaseMetaDataWrapper metaData, Table table) throws SQLException
{
removeInternalPrimaryKeyIndex(metaData, table);
for (int fkIdx = 0; fkIdx < table.getForeignKeyCount(); fkIdx++)
{
removeInternalForeignKeyIndex(metaData, table, table.getForeignKey(fkIdx));
}
}
/**
* Tries to remove the internal index for the table's primary key.
*
* @param metaData The database meta data
* @param table The table
*/
protected void removeInternalPrimaryKeyIndex(DatabaseMetaDataWrapper metaData, Table table) throws SQLException
{
Column[] pks = table.getPrimaryKeyColumns();
List columnNames = new ArrayList();
for (int columnIdx = 0; columnIdx < pks.length; columnIdx++)
{
columnNames.add(pks[columnIdx].getName());
}
for (int indexIdx = 0; indexIdx < table.getIndexCount();)
{
Index index = table.getIndex(indexIdx);
if (index.isUnique() && matches(index, columnNames) &&
isInternalPrimaryKeyIndex(metaData, table, index))
{
table.removeIndex(indexIdx);
}
else
{
indexIdx++;
}
}
}
/**
* Tries to remove the internal index for the given foreign key.
*
* @param metaData The database meta data
* @param table The table where the table is defined
* @param fk The foreign key
*/
protected void removeInternalForeignKeyIndex(DatabaseMetaDataWrapper metaData, Table table, ForeignKey fk) throws SQLException
{
List columnNames = new ArrayList();
boolean mustBeUnique = !getPlatformInfo().isSystemForeignKeyIndicesAlwaysNonUnique();
for (int columnIdx = 0; columnIdx < fk.getReferenceCount(); columnIdx++)
{
String name = fk.getReference(columnIdx).getLocalColumnName();
Column localColumn = table.findColumn(name,
getPlatform().isDelimitedIdentifierModeOn());
if (mustBeUnique && !localColumn.isPrimaryKey())
{
mustBeUnique = false;
}
columnNames.add(name);
}
for (int indexIdx = 0; indexIdx < table.getIndexCount();)
{
Index index = table.getIndex(indexIdx);
if ((!mustBeUnique || index.isUnique()) && matches(index, columnNames) &&
isInternalForeignKeyIndex(metaData, table, fk, index))
{
fk.setAutoIndexPresent(true);
table.removeIndex(indexIdx);
}
else
{
indexIdx++;
}
}
}
/**
* Checks whether the given index matches the column list.
*
* @param index The index
* @param columnsToSearchFor The names of the columns that the index should be for
* @return <code>true</code> if the index matches the columns
*/
protected boolean matches(Index index, List columnsToSearchFor)
{
if (index.getColumnCount() != columnsToSearchFor.size())
{
return false;
}
for (int columnIdx = 0; columnIdx < index.getColumnCount(); columnIdx++)
{
if (!columnsToSearchFor.get(columnIdx).equals(index.getColumn(columnIdx).getName()))
{
return false;
}
}
return true;
}
/**
* Tries to determine whether the index is the internal database-generated index
* for the given table's primary key.
* Note that only unique indices with the correct columns are fed to this method.
* Redefine this method for specific platforms if there are better ways
* to determine internal indices.
*
* @param metaData The database meta data
* @param table The table owning the index
* @param index The index to check
* @return <code>true</code> if the index seems to be an internal primary key one
*/
protected boolean isInternalPrimaryKeyIndex(DatabaseMetaDataWrapper metaData, Table table, Index index) throws SQLException
{
return false;
}
/**
* Tries to determine whether the index is the internal database-generated index
* for the given foreign key.
* Note that only non-unique indices with the correct columns are fed to this method.
* Redefine this method for specific platforms if there are better ways
* to determine internal indices.
*
* @param metaData The database meta data
* @param table The table owning the index and foreign key
* @param fk The foreign key
* @param index The index to check
* @return <code>true</code> if the index seems to be an internal primary key one
*/
protected boolean isInternalForeignKeyIndex(DatabaseMetaDataWrapper metaData, Table table, ForeignKey fk, Index index) throws SQLException
{
return false;
}
/**
* Reads the column definitions for the indicated table.
*
* @param metaData The database meta data
* @param tableName The name of the table
* @return The columns
*/
protected Collection readColumns(DatabaseMetaDataWrapper metaData, String tableName) throws SQLException
{
ResultSet columnData = null;
try
{
columnData = metaData.getColumns(metaData.escapeForSearch(tableName), getDefaultColumnPattern());
List columns = new ArrayList();
while (columnData.next())
{
Map values = readColumns(columnData, getColumnsForColumn());
columns.add(readColumn(metaData, values));
}
return columns;
}
finally
{
closeResultSet(columnData);
}
}
/**
* Extracts a column definition from the result set.
*
* @param metaData The database meta data
* @param values The column meta data values as defined by {@link #getColumnsForColumn()}
* @return The column
*/
protected Column readColumn(DatabaseMetaDataWrapper metaData, Map values) throws SQLException
{
Column column = new Column();
column.setName((String)values.get("COLUMN_NAME"));
column.setDefaultValue((String)values.get("COLUMN_DEF"));
column.setTypeCode(((Integer)values.get("DATA_TYPE")).intValue());
Integer precision = (Integer)values.get("NUM_PREC_RADIX");
if (precision != null)
{
column.setPrecisionRadix(precision.intValue());
}
String size = (String)values.get("COLUMN_SIZE");
if (size == null)
{
size = (String)_defaultSizes.get(new Integer(column.getTypeCode()));
}
// we're setting the size after the precision and radix in case
// the database prefers to return them in the size value
column.setSize(size);
Integer scale = (Integer)values.get("DECIMAL_DIGITS");
if (scale != null)
{
// if there is a scale value, set it after the size (which probably did not contain
// a scale specification)
column.setScale(scale.intValue());
}
column.setRequired("NO".equalsIgnoreCase(((String)values.get("IS_NULLABLE")).trim()));
String description = (String)values.get("REMARKS");
if (!org.apache.ddlutils.util.StringUtilsExt.isEmpty(description))
{
column.setDescription(description);
}
return column;
}
/**
* Retrieves the names of the columns that make up the primary key for a given table.
*
* @param metaData The database meta data
* @param tableName The name of the table from which to retrieve PK information
* @return The primary key column names
*/
protected Collection readPrimaryKeyNames(DatabaseMetaDataWrapper metaData, String tableName) throws SQLException
{
List pks = new ArrayList();
ResultSet pkData = null;
try
{
pkData = metaData.getPrimaryKeys(metaData.escapeForSearch(tableName));
while (pkData.next())
{
Map values = readColumns(pkData, getColumnsForPK());
pks.add(readPrimaryKeyName(metaData, values));
}
}
finally
{
closeResultSet(pkData);
}
return pks;
}
/**
* Extracts a primary key name from the result set.
*
* @param metaData The database meta data
* @param values The primary key meta data values as defined by {@link #getColumnsForPK()}
* @return The primary key name
*/
protected String readPrimaryKeyName(DatabaseMetaDataWrapper metaData, Map values) throws SQLException
{
return (String)values.get("COLUMN_NAME");
}
/**
* Retrieves the foreign keys of the indicated table.
*
* @param metaData The database meta data
* @param tableName The name of the table from which to retrieve FK information
* @return The foreign keys
*/
protected Collection readForeignKeys(DatabaseMetaDataWrapper metaData, String tableName) throws SQLException
{
Map fks = new ListOrderedMap();
ResultSet fkData = null;
try
{
fkData = metaData.getForeignKeys(metaData.escapeForSearch(tableName));
while (fkData.next())
{
Map values = readColumns(fkData, getColumnsForFK());
readForeignKey(metaData, values, fks);
}
}
finally
{
closeResultSet(fkData);
}
return fks.values();
}
/**
* Reads the next foreign key spec from the result set.
*
* @param metaData The database meta data
* @param values The foreign key meta data as defined by {@link #getColumnsForFK()}
* @param knownFks The already read foreign keys for the current table
*/
protected void readForeignKey(DatabaseMetaDataWrapper metaData, Map values, Map knownFks) throws SQLException
{
String fkName = (String)values.get("FK_NAME");
ForeignKey fk = (ForeignKey)knownFks.get(fkName);
if (fk == null)
{
fk = new ForeignKey(fkName);
fk.setForeignTableName((String)values.get("PKTABLE_NAME"));
CascadeActionEnum onUpdateAction = convertAction((Short)values.get("UPDATE_RULE"));
CascadeActionEnum onDeleteAction = convertAction((Short)values.get("DELETE_RULE"));
if (onUpdateAction == null)
{
onUpdateAction = getPlatformInfo().getDefaultOnUpdateAction();
}
if (onDeleteAction == null)
{
onDeleteAction = getPlatformInfo().getDefaultOnDeleteAction();
}
fk.setOnUpdate(onUpdateAction);
fk.setOnDelete(onDeleteAction);
knownFks.put(fkName, fk);
}
Reference ref = new Reference();
ref.setForeignColumnName((String)values.get("PKCOLUMN_NAME"));
ref.setLocalColumnName((String)values.get("FKCOLUMN_NAME"));
if (values.containsKey("KEY_SEQ"))
{
ref.setSequenceValue(((Short)values.get("KEY_SEQ")).intValue());
}
fk.addReference(ref);
}
/**
* Converts the JDBC action value (one of the <code>importKey</code> constants in the
* {@link DatabaseMetaData} class) to a {@link CascadeActionEnum}.
*
* @param jdbcActionValue The jdbc action value
* @return The enum value
*/
protected CascadeActionEnum convertAction(Short jdbcActionValue)
{
CascadeActionEnum action = null;
if (jdbcActionValue != null)
{
switch (jdbcActionValue.shortValue())
{
case DatabaseMetaData.importedKeyCascade:
action = CascadeActionEnum.CASCADE;
break;
case DatabaseMetaData.importedKeySetNull:
action = CascadeActionEnum.SET_NULL;
break;
case DatabaseMetaData.importedKeySetDefault:
action = CascadeActionEnum.SET_DEFAULT;
break;
case DatabaseMetaData.importedKeyRestrict:
action = CascadeActionEnum.RESTRICT;
break;
}
}
return action;
}
/**
* Determines the indices for the indicated table.
*
* @param metaData The database meta data
* @param tableName The name of the table
* @return The list of indices
*/
protected Collection readIndices(DatabaseMetaDataWrapper metaData, String tableName) throws SQLException
{
Map indices = new ListOrderedMap();
ResultSet indexData = null;
try
{
indexData = metaData.getIndices(metaData.escapeForSearch(tableName), false, false);
while (indexData.next())
{
Map values = readColumns(indexData, getColumnsForIndex());
readIndex(metaData, values, indices);
}
}
finally
{
closeResultSet(indexData);
}
return indices.values();
}
/**
* Reads the next index spec from the result set.
*
* @param metaData The database meta data
* @param values The index meta data as defined by {@link #getColumnsForIndex()}
* @param knownIndices The already read indices for the current table
*/
protected void readIndex(DatabaseMetaDataWrapper metaData, Map values, Map knownIndices) throws SQLException
{
Short indexType = (Short)values.get("TYPE");
// we're ignoring statistic indices
if ((indexType != null) && (indexType.shortValue() == DatabaseMetaData.tableIndexStatistic))
{
return;
}
String indexName = (String)values.get("INDEX_NAME");
if (indexName != null)
{
Index index = (Index)knownIndices.get(indexName);
if (index == null)
{
if (((Boolean)values.get("NON_UNIQUE")).booleanValue())
{
index = new NonUniqueIndex();
}
else
{
index = new UniqueIndex();
}
index.setName(indexName);
knownIndices.put(indexName, index);
}
IndexColumn indexColumn = new IndexColumn();
indexColumn.setName((String)values.get("COLUMN_NAME"));
if (values.containsKey("ORDINAL_POSITION"))
{
indexColumn.setOrdinalPosition(((Short)values.get("ORDINAL_POSITION")).intValue());
}
index.addColumn(indexColumn);
}
}
/**
* Reads the indicated columns from the result set.
*
* @param resultSet The result set
* @param columnDescriptors The dscriptors of the columns to read
* @return The read values keyed by the column name
*/
protected Map readColumns(ResultSet resultSet, List columnDescriptors) throws SQLException
{
HashMap values = new HashMap();
for (Iterator it = columnDescriptors.iterator(); it.hasNext();)
{
MetaDataColumnDescriptor descriptor = (MetaDataColumnDescriptor)it.next();
values.put(descriptor.getName(), descriptor.readColumn(resultSet));
}
return values;
}
/**
* Helper method that determines the auto increment status for the given columns via the
* {@link ResultSetMetaData#isAutoIncrement(int)} method.
*
* @param table The table
* @param columnsToCheck The columns to check (e.g. the primary key columns)
*/
protected void determineAutoIncrementFromResultSetMetaData(Table table, Column[] columnsToCheck) throws SQLException
{
if ((columnsToCheck == null) || (columnsToCheck.length == 0))
{
return;
}
StringBuffer query = new StringBuffer();
query.append("SELECT ");
for (int idx = 0; idx < columnsToCheck.length; idx++)
{
if (idx > 0)
{
query.append(",");
}
if (getPlatform().isDelimitedIdentifierModeOn())
{
query.append(getPlatformInfo().getDelimiterToken());
}
query.append(columnsToCheck[idx].getName());
if (getPlatform().isDelimitedIdentifierModeOn())
{
query.append(getPlatformInfo().getDelimiterToken());
}
}
query.append(" FROM ");
if (getPlatform().isDelimitedIdentifierModeOn())
{
query.append(getPlatformInfo().getDelimiterToken());
}
query.append(table.getName());
if (getPlatform().isDelimitedIdentifierModeOn())
{
query.append(getPlatformInfo().getDelimiterToken());
}
query.append(" WHERE 1 = 0");
Statement stmt = null;
try
{
stmt = getConnection().createStatement();
ResultSet rs = stmt.executeQuery(query.toString());
ResultSetMetaData rsMetaData = rs.getMetaData();
for (int idx = 0; idx < columnsToCheck.length; idx++)
{
if (rsMetaData.isAutoIncrement(idx + 1))
{
columnsToCheck[idx].setAutoIncrement(true);
}
}
}
finally
{
closeStatement(stmt);
}
}
/**
* Sorts the foreign keys in the tables of the model.
*
* @param model The model
*/
protected void sortForeignKeys(Database model)
{
for (int tableIdx = 0; tableIdx < model.getTableCount(); tableIdx++)
{
model.getTable(tableIdx).sortForeignKeys(getPlatform().isDelimitedIdentifierModeOn());
}
}
/**
* Replaces a specific character sequence in the given text with the character sequence
* whose escaped version it is.
*
* @param text The text
* @param unescaped The unescaped string, e.g. "'"
* @param escaped The escaped version, e.g. "''"
* @return The resulting text
*/
protected String unescape(String text, String unescaped, String escaped)
{
String result = text;
// we need special handling if the single quote is escaped via a double single quote
if (result != null)
{
if (escaped.equals("''"))
{
if ((result.length() > 2) && result.startsWith("'") && result.endsWith("'"))
{
result = "'" + StringUtils.replace(result.substring(1, result.length() - 1), escaped, unescaped) + "'";
}
else
{
result = StringUtils.replace(result, escaped, unescaped);
}
}
else
{
result = StringUtils.replace(result, escaped, unescaped);
}
}
return result;
}
/**
* Tries to find the schema to which the given table belongs.
*
* @param connection The database connection
* @param schemaPattern The schema pattern to limit the schemas to search in
* @param table The table to search for
* @return The schema name or <code>null</code> if the schema of the table
* could not be found
* @deprecated Will be removed once full schema support is in place
*/
public String determineSchemaOf(Connection connection, String schemaPattern, Table table) throws SQLException
{
ResultSet tableData = null;
ResultSet columnData = null;
try
{
DatabaseMetaDataWrapper metaData = new DatabaseMetaDataWrapper();
metaData.setMetaData(connection.getMetaData());
metaData.setCatalog(getDefaultCatalogPattern());
metaData.setSchemaPattern(schemaPattern == null ? getDefaultSchemaPattern() : schemaPattern);
metaData.setTableTypes(getDefaultTableTypes());
String tablePattern = table.getName();
if (getPlatform().isDelimitedIdentifierModeOn())
{
tablePattern = tablePattern.toUpperCase();
}
tableData = metaData.getTables(metaData.escapeForSearch(tablePattern));
boolean found = false;
String schema = null;
while (!found && tableData.next())
{
Map values = readColumns(tableData, getColumnsForTable());
String tableName = (String)values.get("TABLE_NAME");
if ((tableName != null) && (tableName.length() > 0))
{
schema = (String)values.get("TABLE_SCHEM");
columnData = metaData.getColumns(metaData.escapeForSearch(tableName), getDefaultColumnPattern());
found = true;
while (found && columnData.next())
{
values = readColumns(columnData, getColumnsForColumn());
if (table.findColumn((String)values.get("COLUMN_NAME"),
getPlatform().isDelimitedIdentifierModeOn()) == null)
{
found = false;
}
}
columnData.close();
columnData = null;
}
}
return found ? schema : null;
}
finally
{
closeResultSet(columnData);
closeResultSet(tableData);
}
}
/**
* Closes the given result set. Any exceptions are logged on warn level but otherwise swallowed.
*
* @param resultSet The result set
*/
protected void closeResultSet(ResultSet resultSet)
{
if (resultSet != null)
{
try
{
resultSet.close();
}
catch (SQLException ex)
{
_log.warn("Error while closing result set", ex);
}
}
}
/**
* Closes the given statement. Any exceptions are logged on warn level but otherwise swallowed.
*
* @param stmt The statement
*/
protected void closeStatement(Statement stmt)
{
if (stmt != null)
{
try
{
stmt.close();
}
catch (SQLException ex)
{
_log.warn("Error while closing statement", ex);
}
}
}
}