/*
* <ATGCOPYRIGHT> Copyright (C) 2009 Art Technology Group, Inc. All Rights
* Reserved. No use, copying or distribution of this work may be made except in
* accordance with a valid license agreement from Art Technology Group. This
* notice must be included on all copies, modifications and derivatives of this
* work.
*
* Art Technology Group (ATG) MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE
* SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE, OR NON-INFRINGEMENT. ATG SHALL NOT BE LIABLE FOR ANY
* DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
* THIS SOFTWARE OR ITS DERIVATIVES.
*
* "Dynamo" is a trademark of Art Technology Group, Inc. </ATGCOPYRIGHT>
*/
package atg.adapter.gsa;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import org.apache.ddlutils.DatabaseOperationException;
import org.apache.ddlutils.Platform;
import org.apache.ddlutils.PlatformFactory;
import org.apache.ddlutils.model.Column;
import org.apache.ddlutils.model.Database;
import org.apache.ddlutils.model.ForeignKey;
import org.apache.ddlutils.model.IndexColumn;
import org.apache.ddlutils.model.Reference;
import org.apache.ddlutils.model.UniqueIndex;
import org.apache.log4j.Logger;
import atg.repository.RepositoryException;
/**
* This class is used to generate drop and alter a database schema required for
* a given repository. It uses the Apache DDLUtils tools for the actual schema
* manipulation. To use the class first initialize it's "model" by passing a
* GSARepository to the constructor. Afterwards you may invoke the action
* methods such as:
* <ul>
* <li>createSchema - Creates schema including constraints.
* <li>dropSchema - Drops the schema including constraints.
* <li>alterSchema - Attempts to alter an existing schema into the one currently
* required for the given repository.
* </ul>
* These methods affect the DataSource used by the given GSARepository. If that
* DataSource is not accessible then these methods will fail. Schema
* modification may continue or fail on error. Set the <code>strict</code>
* property to true to always fail on error. The default is to continue on
* error.
*
* @version $Id: //test/UnitTests/base/main/src/Java/atg/adapter/gsa/
* GSARepositorySchemaGenerator.java#1 $
* @author adamb
*/
public class GSARepositorySchemaGenerator {
Logger mLogger = Logger.getLogger(GSARepositorySchemaGenerator.class);
// The repository upon which we are working.
public GSARepository mRepository = null;
// The DDLUtils Platform object
public Platform mPlatform = null;
// The DDLUtils Database Model
public Database mDatabase = null;
// Tool for mapping database types
public DatabaseTypeNameToJDBC mDatabaseTypeNameToJDBC = null;
static Set mUsedFKNames = new HashSet();
// -----------------------------
/**
* Creates a new GSARepositorySchemaGenerator and initializes it with a model
* based upon the given repository.
*
* @param pRepository
* @param pIncludeExistingTables If true, the model will include the existing database tables as well as
* tables from the current repository.
* @throws RepositoryException
*/
public GSARepositorySchemaGenerator(GSARepository pRepository,boolean pIncludeExistingTables) throws RepositoryException {
buildModel(pRepository,pIncludeExistingTables);
}
// -----------------------------
/**
* Creates a new GSARepositorySchemaGenerator and initializes it with a model
* based upon the given repository.
*
* @param pRepository
* @throws RepositoryException
*/
public GSARepositorySchemaGenerator(GSARepository pRepository) throws RepositoryException {
buildModel(pRepository,false);
}
// -----------------------------
/**
* Initialize this class with a model for the given repository. Any previous
* model will be discarded.
*
* @param pRepository
* @throws RepositoryException
*/
public void buildModel(GSARepository pRepository) throws RepositoryException {
buildModel(pRepository,false);
}
// -----------------------------
/**
* Initialize this class with a model for the given repository. Any previous
* model will be discarded.
*
* @param pRepository
* @param pIncludeExistingTables If true the existing tables in the database will
* be added to the model.
* @throws RepositoryException
*/
public void buildModel(GSARepository pRepository, boolean pIncludeExistingTables) throws RepositoryException {
mDatabaseTypeNameToJDBC = new DatabaseTypeNameToJDBC(pRepository.getDatabaseTableInfo());
mRepository = pRepository;
mPlatform = PlatformFactory.createNewPlatformInstance(pRepository.getDataSource());
if (pIncludeExistingTables) {
mDatabase = mPlatform.readModelFromDatabase(pRepository.getAbsoluteName());
} else {
mDatabase = new Database();
mDatabase.setName(pRepository.getAbsoluteName());
mDatabase.setVersion("1.0");
}
String[] names = pRepository.getItemDescriptorNames();
for (String name : names) {
GSAItemDescriptor desc = (GSAItemDescriptor) pRepository
.getItemDescriptor(name);
Table[] tables = desc.getTables();
// first do primary tables
processTables(pRepository, tables, true);
}
for (String name : names) {
GSAItemDescriptor desc = (GSAItemDescriptor) pRepository
.getItemDescriptor(name);
Table[] tables = desc.getTables();
// then do the rest
desc.getPrimaryTable();
processTables(pRepository, tables, false);
}
}
/**
* Walks the tables of this repository building up a DDLUtils model.
*
* @param pRepository
* @param tables
* @param pPrimary
* - if True only processes primary tables
* @throws RepositoryException
*/
private void processTables(GSARepository pRepository, Table[] tables,
boolean pPrimary) throws RepositoryException {
for (Table table : tables) {
if (!table.isInherited() && (table.isPrimaryTable() == pPrimary)) {
// track tables here. if we have multiple repositories
// using the same table we don't want to double create.
// actually the problem is more a single
// repository that is reusing a table for multiple
// purposes
List<GSARepository> repositoriesUsingTable = SchemaTracker
.getSchemaTracker().getTableToRepository().get(table.getName());
// Skip if we have added this table
// and it is already in the model
// Actually, checking the model
// is probably enough
if (repositoriesUsingTable != null
&& (mDatabase.findTable(table.getName()) != null)
) {
if (pRepository.isLoggingDebug())
pRepository.logDebug("Table " + table.getName()
+ " already defined by repository "
+ repositoriesUsingTable.toString()
+ " skipping schema creation for this table. multi="
+ table.isMultiTable() + " auxiliary=" + table.isAuxiliaryTable()
+ " primary=" + table.isPrimaryTable());
if (!repositoriesUsingTable.contains(pRepository))
repositoriesUsingTable.add(pRepository);
} else {
// Only add the model if we have never seen this table created
buildSingleTableModel(mDatabase, table, pRepository);
repositoriesUsingTable = new ArrayList<GSARepository>();
repositoriesUsingTable.add(pRepository);
}
SchemaTracker.getSchemaTracker().getTableToRepository().put(
table.getName(), repositoriesUsingTable);
}
}
}
// -----------------------------
/**
* Adds the definition of the given table to the current DDLUtils database
* model.
*
* @param pDb
* @param pTable
* @param pRepository
* @throws RepositoryException
*/
void buildSingleTableModel(Database pDb, Table pTable,
GSARepository pRepository) throws RepositoryException {
TableColumns columns = new TableColumns(pTable, pRepository
.getDatabaseTableInfo());
pTable.collectColumnsForName(columns);
AccessibleTableColumns atable = new AccessibleTableColumns(columns);
// --------------------------
// Table Definition
// --------------------------
org.apache.ddlutils.model.Table t = new org.apache.ddlutils.model.Table();
t.setName(pTable.getName());
pDb.addTable(t);
// --------------------------
// Add Columns
// --------------------------
ColumnDefinitionNode columnDefinition = null;
boolean proceed = false;
for (columnDefinition = atable.getHead(), proceed = true; columnDefinition != null
&& proceed; columnDefinition = columnDefinition.mNext) {
// No need to iterate the next time if there is just one element in the
// linked list
if (atable.getHead() == atable.getTail())
proceed = false;
Column c = new Column();
// --------------------------
// Column Name
// --------------------------
c.setName(columnDefinition.mColumnName);
t.addColumn(c);
// --------------------------
// Column Type
// --------------------------
setupColumnType(pRepository, columnDefinition, c);
// --------------------------
// Primary Key
// --------------------------
if (atable.getPrimaryKeys().contains(c.getName())
|| c.getName().equals(atable.getMultiColumnName())) {
c.setPrimaryKey(true);
}
// --------------------------
// Null/NotNull
// --------------------------
if (columnDefinition.mIsRequired
|| atable.getPrimaryKeys().contains(columnDefinition.mColumnName)) {
c.setRequired(true);
} else {
c.setRequired(false);
}
// --------------------------
// Unique Index
// DDLUtils doesn't yet to UNIQUE constraints.. Hmph
// --------------------------
if (columnDefinition.mIsUnique) {
UniqueIndex uniqueIndex = new UniqueIndex();
uniqueIndex.setName("uidx_" + t.getName() + "_" + c.getName());
uniqueIndex.addColumn(new IndexColumn(c));
t.addIndex(uniqueIndex);
}
// --------------------------
// References Constraint
// --------------------------
if (columnDefinition.mReferenced != null && !columns.mVersioned) {
ForeignKey foreignKey = new ForeignKey();
Reference reference = new Reference();
String referencedTableName = columnDefinition.mReferenced.substring(0,
columnDefinition.mReferenced.indexOf("("));
String referencedColumnName = columnDefinition.mReferenced.substring(
columnDefinition.mReferenced.indexOf("(") + 1,
columnDefinition.mReferenced.indexOf(")"));
org.apache.ddlutils.model.Table referencedTable = pDb
.findTable(referencedTableName);
String fkName = (t.getName() + c.getName() + "FK"
+ referencedTableName + referencedColumnName);
foreignKey.setName(fkName);
if (referencedTable != null) {
Column referencedColumn = referencedTable
.findColumn(referencedColumnName);
if (referencedTable.getName().equals(t.getName())
&& pRepository.isLoggingDebug()
&& referencedColumn.getName().equals(c.getName())) {
if (pRepository.isLoggingDebug())
pRepository
.logDebug("Skipping foreign key constraint, table and column are the same. Table.Column="
+ referencedTableName + "." + referencedColumnName);
} else {
reference.setForeignColumn(referencedColumn);
reference.setLocalColumn(c);
foreignKey.addReference(reference);
foreignKey.setForeignTable(referencedTable);
// try to find existing fk
ForeignKey existingKey = t.findForeignKey(foreignKey);
// don't add this fk if the name is already used
if (existingKey == null)
t.addForeignKey(foreignKey);
}
} else {
if (pRepository.isLoggingDebug())
pRepository.logDebug("skipping adding fk, referenced table is null" + fkName);
}
// --------------------------
// Foreign Keys
// --------------------------
if (atable.getForeignKeys() != null && !columns.mVersioned) {
// TODO: Add ForeignKeys
}
}
}
}
/**
* Determines the appropriate jdbc type for the given ColumnDefinitionNode and
* sets that in Column "c".
*
* @param pRepository
* @param columnDefinition
* @param c
*/
void setupColumnType(GSARepository pRepository,
ColumnDefinitionNode columnDefinition, Column c) {
c.setDescription(columnDefinition.mDataTypeString);
String typeName = null;
String size = null;
if (columnDefinition.mDataTypeString.contains("(")) {
typeName = columnDefinition.mDataTypeString.substring(0,
columnDefinition.mDataTypeString.indexOf("("));
size = columnDefinition.mDataTypeString.substring(
columnDefinition.mDataTypeString.indexOf("(") + 1,
columnDefinition.mDataTypeString.indexOf(")"));
} else {
typeName = columnDefinition.mDataTypeString;
}
String precision = null;
String scale = null;
if (size != null) {
if (size.contains(",")) {
precision = size.substring(0, size.indexOf(","));
scale = size.substring(size.indexOf(",") + 1, size.length());
c.setPrecisionRadix(Integer.parseInt(precision.trim()));
c.setScale(Integer.parseInt(scale.trim()));
} else {
c.setSize(size);
}
}
c.setTypeCode(mDatabaseTypeNameToJDBC.databaseTypeNametoJDBCType(typeName));
}
// -----------------------------
/**
* Creates the schema based on the current model. If no model has been
* created, this method throws a NoModelException.
*
* @param pContinueOnError
* - If true, continue on error, else fail.
* @param pDrop
* - If true, drops schema first before attempting to create it.
* @throws SQLException
* @throws DatabaseOperationException
*/
public void createSchema(final boolean pContinueOnError, final boolean pDrop)
throws DatabaseOperationException, SQLException {
boolean success = new DoInAutoCommit(this, mRepository).doInAutoCommit(new AutoCommitable() {
public void doInAutoCommit(Connection pConnection) {
mPlatform.createTables(pConnection, mDatabase, pDrop, pContinueOnError);
}
});
if (!success)
throw new DatabaseOperationException("Failed to create tables.");
}
// -----------------------------
/**
* Drops the schema based on the current model. If no model has been created,
* this method throws a NoModelException.
*
* @param pContinueOnError
* - If true, continue on error, else fail.
* @throws SQLException
* @throws DatabaseOperationException
*/
public void dropSchema(final boolean pContinueOnError) throws DatabaseOperationException, SQLException {
mLogger.debug("dropSchema(): Begin");
boolean success = new DoInAutoCommit(this, mRepository)
.doInAutoCommit(new AutoCommitable() {
public void doInAutoCommit(Connection pConnection) {
mPlatform.dropTables(pConnection, mDatabase, pContinueOnError);
}
});
if (!success)
throw new DatabaseOperationException("Failed to drop tables.");
}
// -----------------------------
/**
* Alters the schema based on the current model. If no model has been created,
* this method throws a NoModelException. This method attempts to preserve the
* data in the target database.
*
* @param pContinueOnError
* - If true, fail on error, else continue on error.
* @throws SQLException
* @throws DatabaseOperationException
*/
public void alterSchema(final boolean pContinueOnError) throws DatabaseOperationException, SQLException {
mPlatform.alterTables(mRepository.getConnection(), mDatabase, pContinueOnError);
boolean success = new DoInAutoCommit(this, mRepository)
.doInAutoCommit(new AutoCommitable() {
public void doInAutoCommit(Connection pConnection) {
mPlatform.alterTables(pConnection, mDatabase, pContinueOnError);
}
});
if (!success)
throw new DatabaseOperationException("Failed to alter tables.");
}
}