/*
* Copyright 2013 Matt Sicker and Contributors
*
* Licensed 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.
*/
package atg.tools.dynunit.adapter.gsa;
import atg.adapter.gsa.ColumnDefinitionNode;
import atg.adapter.gsa.GSAItemDescriptor;
import atg.adapter.gsa.GSARepository;
import atg.adapter.gsa.Table;
import atg.adapter.gsa.TableColumns;
import atg.repository.RepositoryException;
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.jetbrains.annotations.Nullable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 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.
*
* @author adamb
* @version $Id: //test/UnitTests/base/main/src/Java/atg/adapter/gsa/
* GSARepositorySchemaGenerator.java#1 $
*/
public class GSARepositorySchemaGenerator {
// The repository upon which we are working.
@Nullable
private GSARepository mRepository = null;
// The DDLUtils Platform object
private Platform mPlatform = null;
// The DDLUtils Database Model
private Database mDatabase = null;
// Tool for mapping database types
private 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.getInstance().getTable(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.getInstance().putTable(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 {
// XXX: holy shitwaffles simplify this shit
TableColumns columns = new TableColumns(
pTable, pRepository.getDatabaseTableInfo()
);
pTable.collectColumnsForName(columns);
AccessibleTableColumns accessibleTableColumns = 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 = accessibleTableColumns.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 ( accessibleTableColumns.getHead() == accessibleTableColumns.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 ( accessibleTableColumns.getPrimaryKeys().contains(c.getName()) || c.getName()
.equals(accessibleTableColumns.getMultiColumnName()) ) {
c.setPrimaryKey(true);
}
// --------------------------
// Null/NotNull
// --------------------------
if ( columnDefinition.mIsRequired || accessibleTableColumns.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 ( accessibleTableColumns.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 DatabaseOperationException
*/
public void createSchema(final boolean pContinueOnError, final boolean pDrop)
throws DatabaseOperationException {
boolean success = new DoInAutoCommit(this, mRepository).doInAutoCommit(
new AutoCommitable() {
@Override
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 DatabaseOperationException
*/
public void dropSchema(final boolean pContinueOnError)
throws DatabaseOperationException {
boolean success = new DoInAutoCommit(this, mRepository).doInAutoCommit(
new AutoCommitable() {
@Override
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() {
@Override
public void doInAutoCommit(Connection pConnection) {
mPlatform.alterTables(pConnection, mDatabase, pContinueOnError);
}
}
);
if ( !success ) {
throw new DatabaseOperationException("Failed to alter tables.");
}
}
}