/*
* (c) Copyright 2010-2011 AgileBirds
*
* This file is part of OpenFlexo.
*
* OpenFlexo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenFlexo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.apache.cayenne.access;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
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.ListIterator;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.conn.DataSourceInfo;
import org.apache.cayenne.conn.DriverDataSource;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.dba.PkGenerator;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.DerivedDbEntity;
import org.apache.cayenne.validation.SimpleValidationFailure;
import org.apache.cayenne.validation.ValidationResult;
/**
* Utility class that generates database schema based on Cayenne mapping. It is a logical counterpart of DbLoader class.
*
* @author Andrus Adamchik
*/
public class MyDBGenerator {
private static final Logger logger = Logger.getLogger(MyDBGenerator.class.getPackage().getName());
protected DbAdapter adapter;
protected DataMap map;
// optional DataDomain needed for correct FK generation in cross-db situations
protected DataDomain domain;
// stores generated SQL statements
protected Map<String, String> dropTables;
protected Map<String, String> createTables;
protected Map<String, List<String>> createFK;
protected List<String> createPK;
protected List<String> dropPK;
/**
* Contains all DbEntities ordered considering their interdependencies. DerivedDbEntities are filtered out of this list.
*/
protected List dbEntitiesInInsertOrder;
protected List dbEntitiesRequiringAutoPK;
protected boolean shouldDropTables;
protected boolean shouldCreateTables;
protected boolean shouldDropPKSupport;
protected boolean shouldCreatePKSupport;
protected boolean shouldCreateFKConstraints;
protected ValidationResult failures;
private Comparator dbEntityComparator;
/**
* Creates and initializes new DbGenerator.
*/
public MyDBGenerator(DbAdapter adapt, DataMap _map) {
this(adapt, _map, Collections.EMPTY_LIST);
}
/**
* Creates and initializes new DbGenerator instance.
*
* @param adapter
* DbAdapter corresponding to the database
* @param map
* DataMap whose entities will be used in schema generation
* @param excludedEntities
* entities that should be ignored during schema generation
*/
public MyDBGenerator(DbAdapter _adapter, DataMap _map, Collection excludedEntities) {
this(_adapter, _map, excludedEntities, null, null);
}
/**
* Creates and initializes new DbGenerator instance.
*
* @param adapter
* DbAdapter corresponding to the database
* @param map
* DataMap whose entities will be used in schema generation
* @param excludedEntities
* entities that should be ignored during schema generation
* @param domain
* optional DataDomain used to detect cross-database relationships.
* @since 1.2
*/
public MyDBGenerator(DbAdapter _adapter, DataMap _map, Collection excludedEntities, DataDomain _domain) {
this(_adapter, _map, excludedEntities, _domain, null);
}
/**
* Creates and initializes new DbGenerator instance.
*
* @param excludedEntities
* entities that should be ignored during schema generation
* @param entitySorter
* TODO
* @param adapter
* DbAdapter corresponding to the database
* @param map
* DataMap whose entities will be used in schema generation
* @param domain
* optional DataDomain used to detect cross-database relationships.
*
* @since 1.2
*/
public MyDBGenerator(DbAdapter _adapter, DataMap _map, Collection excludedEntities, DataDomain _domain, Comparator entitySorter) {
// sanity check
if (_adapter == null) {
throw new IllegalArgumentException("Adapter must not be null.");
}
if (_map == null) {
throw new IllegalArgumentException("DataMap must not be null.");
}
this.domain = _domain;
this.map = _map;
this.adapter = _adapter;
this.dbEntityComparator = entitySorter;
prepareDbEntities(excludedEntities);
resetToDefaults();
buildStatements();
}
protected void resetToDefaults() {
this.shouldDropTables = false;
this.shouldDropPKSupport = false;
this.shouldCreatePKSupport = true;
this.shouldCreateTables = true;
this.shouldCreateFKConstraints = true;
}
/**
* Creates and stores internally a set of statements for database schema creation, ignoring configured schema creation preferences.
* Statements are NOT executed in this method.
*/
protected void buildStatements() {
dropTables = new HashMap<String, String>();
createTables = new HashMap<String, String>();
createFK = new HashMap<String, List<String>>();
DbAdapter _adapter = getAdapter();
Iterator it = dbEntitiesInInsertOrder.iterator();
boolean supportsFK = adapter.supportsFkConstraints();
while (it.hasNext()) {
DbEntity dbe = (DbEntity) it.next();
String name = dbe.getName();
// build "DROP TABLE"
dropTables.put(name, _adapter.dropTable(dbe));
// build "CREATE TABLE"
createTables.put(name, _adapter.createTable(dbe));
// build "FK"
if (supportsFK) {
createFK.put(name, createFkConstraintsQueries(dbe));
}
}
PkGenerator pkGenerator = _adapter.getPkGenerator();
dropPK = pkGenerator.dropAutoPkStatements(dbEntitiesRequiringAutoPK);
createPK = pkGenerator.createAutoPkStatements(dbEntitiesRequiringAutoPK);
}
/**
* Returns <code>true</code> if there is nothing to be done by this generator. If <code>respectConfiguredSettings</code> is
* <code>true</code>, checks are done applying currently configured settings, otherwise check is done, assuming that all possible
* generated objects.
*/
public boolean isEmpty(boolean respectConfiguredSettings) {
if (dbEntitiesInInsertOrder.isEmpty() && dbEntitiesRequiringAutoPK.isEmpty()) {
return true;
}
if (!respectConfiguredSettings) {
return false;
}
return !(shouldDropTables || shouldCreateTables || shouldCreateFKConstraints || shouldCreatePKSupport || shouldDropPKSupport);
}
/** Returns DbAdapter associated with this DbGenerator. */
public DbAdapter getAdapter() {
return adapter;
}
/**
* Returns a list of all schema statements that should be executed with the current configuration.
*/
public List<String> configuredStatements() {
List<String> list = new ArrayList<String>();
if (shouldDropTables) {
Iterator it = dbEntitiesInInsertOrder.iterator();
while (it.hasNext()) {
DbEntity ent = (DbEntity) it.next();
list.add(dropTables.get(ent.getName()));
}
}
if (shouldCreateTables) {
ListIterator it = dbEntitiesInInsertOrder.listIterator(dbEntitiesInInsertOrder.size());
while (it.hasPrevious()) {
DbEntity ent = (DbEntity) it.previous();
list.add(createTables.get(ent.getName()));
}
}
if (shouldCreateFKConstraints && getAdapter().supportsFkConstraints()) {
ListIterator it = dbEntitiesInInsertOrder.listIterator(dbEntitiesInInsertOrder.size());
while (it.hasPrevious()) {
DbEntity ent = (DbEntity) it.previous();
List<String> fks = createFK.get(ent.getName());
list.addAll(fks);
}
}
if (shouldDropPKSupport) {
list.addAll(dropPK);
}
if (shouldCreatePKSupport) {
list.addAll(createPK);
}
return list;
}
/**
* Creates a temporary DataSource out of DataSourceInfo and invokes <code>public void runGenerator(DataSource ds)</code>.
*/
public void runGenerator(DataSourceInfo dsi) throws Exception {
this.failures = null;
// do a pre-check. Maybe there is no need to run anything
// and therefore no need to create a connection
if (isEmpty(true)) {
return;
}
Driver driver = (Driver) Class.forName(dsi.getJdbcDriver()).newInstance();
DataSource dataSource = new DriverDataSource(driver, dsi.getDataSourceUrl(), dsi.getUserName(), dsi.getPassword());
runGenerator(dataSource);
}
/**
* Executes a set of commands to drop/create database objects. This is the main worker method of DbGenerator. Command set is built based
* on pre-configured generator settings.
*/
public void runGenerator(DataSource ds) throws Exception {
this.failures = null;
Connection connection = ds.getConnection();
try {
// drop tables
if (shouldDropTables) {
ListIterator it = dbEntitiesInInsertOrder.listIterator(dbEntitiesInInsertOrder.size());
while (it.hasPrevious()) {
DbEntity ent = (DbEntity) it.previous();
safeExecute(connection, dropTables.get(ent.getName()));
}
}
// create tables
List createdTables = new ArrayList();
if (shouldCreateTables) {
Iterator it = dbEntitiesInInsertOrder.iterator();
while (it.hasNext()) {
DbEntity ent = (DbEntity) it.next();
// only create missing tables
safeExecute(connection, createTables.get(ent.getName()));
createdTables.add(ent.getName());
}
}
// create FK
if (shouldCreateTables && shouldCreateFKConstraints && getAdapter().supportsFkConstraints()) {
Iterator it = dbEntitiesInInsertOrder.iterator();
while (it.hasNext()) {
DbEntity ent = (DbEntity) it.next();
if (createdTables.contains(ent.getName())) {
List fks = createFK.get(ent.getName());
Iterator fkIt = fks.iterator();
while (fkIt.hasNext()) {
safeExecute(connection, (String) fkIt.next());
}
}
}
}
// drop PK
if (shouldDropPKSupport) {
List dropAutoPKSQL = getAdapter().getPkGenerator().dropAutoPkStatements(dbEntitiesRequiringAutoPK);
Iterator it = dropAutoPKSQL.iterator();
while (it.hasNext()) {
safeExecute(connection, (String) it.next());
}
}
// create pk
if (shouldCreatePKSupport) {
List createAutoPKSQL = getAdapter().getPkGenerator().createAutoPkStatements(dbEntitiesRequiringAutoPK);
Iterator it = createAutoPKSQL.iterator();
while (it.hasNext()) {
safeExecute(connection, (String) it.next());
}
}
} finally {
connection.close();
}
}
/**
* Builds and executes a SQL statement, catching and storing SQL exceptions resulting from invalid SQL. Only non-recoverable exceptions
* are rethrown.
*
* @since 1.1
*/
protected boolean safeExecute(Connection connection, String sql) throws SQLException {
Statement statement = connection.createStatement();
try {
QueryLogger.logQuery(sql, null);
statement.execute(sql);
return true;
} catch (SQLException ex) {
if (this.failures == null) {
this.failures = new ValidationResult();
}
failures.addFailure(new SimpleValidationFailure(sql, ex.getMessage()));
QueryLogger.logQueryError(ex);
return false;
} finally {
statement.close();
}
}
/**
* Returns an array of queries to create foreign key constraints for a particular DbEntity. Throws CayenneRuntimeException, if called
* for adapter that does not support FK constraints.
*/
public List<String> createFkConstraintsQueries(DbEntity dbEnt) {
if (!getAdapter().supportsFkConstraints()) {
throw new CayenneRuntimeException("FK constraints are not supported by adapter.");
}
List<String> list = new ArrayList<String>();
Iterator it = dbEnt.getRelationships().iterator();
while (it.hasNext()) {
DbRelationship rel = (DbRelationship) it.next();
if (rel.isToMany()) {
continue;
}
// skip FK to a different DB
if (domain != null) {
DataMap srcMap = rel.getSourceEntity().getDataMap();
DataMap targetMap = rel.getTargetEntity().getDataMap();
if (srcMap != null && targetMap != null && srcMap != targetMap) {
if (domain.lookupDataNode(srcMap) != domain.lookupDataNode(targetMap)) {
continue;
}
}
}
// create an FK CONSTRAINT only if the relationship is to PK
// and if this is not a dependent PK
// create UNIQUE CONSTRAINT on FK if reverse relationship is to-one
if (rel.isToPK() && !rel.isToDependentPK()) {
if (getAdapter().supportsUniqueConstraints()) {
DbRelationship reverse = rel.getReverseRelationship();
if (reverse != null && !reverse.isToMany() && !reverse.isToPK()) {
list.add(getAdapter().createUniqueConstraint((DbEntity) rel.getSourceEntity(), rel.getSourceAttributes()));
}
}
list.add(getAdapter().createFkConstraint(rel));
}
}
return list;
}
/**
* Returns an object representing a collection of failures that occurred on the last "runGenerator" invocation, or null if there were no
* failures. Failures usually indicate problems with generated DDL (such as "create...", "drop...", etc.) and usually happen due to the
* DataMap being out of sync with the database.
*
* @since 1.1
*/
public ValidationResult getFailures() {
return failures;
}
/**
* Returns whether DbGenerator is configured to create primary key support for DataMap entities.
*/
public boolean shouldCreatePKSupport() {
return shouldCreatePKSupport;
}
/**
* Returns whether DbGenerator is configured to create tables for DataMap entities.
*/
public boolean shouldCreateTables() {
return shouldCreateTables;
}
public boolean shouldDropPKSupport() {
return shouldDropPKSupport;
}
public boolean shouldDropTables() {
return shouldDropTables;
}
public boolean shouldCreateFKConstraints() {
return shouldCreateFKConstraints;
}
public void setShouldCreatePKSupport(boolean shouldCreatePKSupport) {
this.shouldCreatePKSupport = shouldCreatePKSupport;
}
public void setShouldCreateTables(boolean shouldCreateTables) {
this.shouldCreateTables = shouldCreateTables;
}
public void setShouldDropPKSupport(boolean shouldDropPKSupport) {
this.shouldDropPKSupport = shouldDropPKSupport;
}
public void setShouldDropTables(boolean shouldDropTables) {
this.shouldDropTables = shouldDropTables;
}
public void setShouldCreateFKConstraints(boolean shouldCreateFKConstraints) {
this.shouldCreateFKConstraints = shouldCreateFKConstraints;
}
/**
* Returns a DataDomain used by the DbGenerator to detect cross-database relationships. By default DataDomain is null.
*
* @since 1.2
*/
public DataDomain getDomain() {
return domain;
}
/**
* Helper method that orders DbEntities to satisfy referential constraints and returns an ordered list. It also filters out
* DerivedDbEntities.
*/
private void prepareDbEntities(Collection excludedEntities) {
if (excludedEntities == null) {
excludedEntities = Collections.EMPTY_LIST;
}
// remove derived db entities
Vector<DbEntity> tables = new Vector<DbEntity>();
List<DbEntity> tablesWithAutoPk = new ArrayList<DbEntity>();
Iterator it = map.getDbEntities().iterator();
while (it.hasNext()) {
DbEntity nextEntity = (DbEntity) it.next();
// do sanity checks...
// derived DbEntities are not included in generated SQL
if (nextEntity instanceof DerivedDbEntity) {
continue;
}
// tables with no columns are not included
if (nextEntity.getAttributes().size() == 0) {
logger.info("Skipping entity with no attributes: " + nextEntity.getName());
continue;
}
// check if this entity is explicitly excluded
if (excludedEntities.contains(nextEntity)) {
continue;
}
// tables with invalid DbAttributes are not included
boolean invalidAttributes = false;
Iterator nextDbAtributes = nextEntity.getAttributes().iterator();
while (nextDbAtributes.hasNext()) {
DbAttribute attr = (DbAttribute) nextDbAtributes.next();
if (attr.getType() == TypesMapping.NOT_DEFINED) {
logger.info("Skipping entity, attribute type is undefined: " + nextEntity.getName() + "." + attr.getName());
invalidAttributes = true;
break;
}
}
if (invalidAttributes) {
continue;
}
tables.add(nextEntity);
// check if an automatic PK generation can be potentailly supported
// in this entity. For now simply check that the key is not propagated
Iterator relationships = nextEntity.getRelationships().iterator();
// create a copy of the original PK list,
// since the list will be modified locally
List pkAttributes = new ArrayList(nextEntity.getPrimaryKey());
while (pkAttributes.size() > 0 && relationships.hasNext()) {
DbRelationship nextRelationship = (DbRelationship) relationships.next();
if (!nextRelationship.isToMasterPK()) {
continue;
}
// supposedly all source attributes of the relationship
// to master entity must be a part of primary key,
// so
Iterator joins = nextRelationship.getJoins().iterator();
while (joins.hasNext()) {
DbJoin join = (DbJoin) joins.next();
pkAttributes.remove(join.getSource());
}
}
// primary key is needed only if at least one of the primary key attributes
// is not propagated via relationship
if (pkAttributes.size() == 1) {
// GPO add: check that the PK is an integer
boolean needAutoPK = false;
Iterator i = pkAttributes.iterator();
while (i.hasNext()) {
DbAttribute att = (DbAttribute) i.next();
switch (att.getType()) {
case Types.INTEGER:
case Types.NUMERIC:
case Types.BIGINT:
case Types.TINYINT:
case Types.SMALLINT:
needAutoPK = true;
break;
default:
break;
}
}
if (needAutoPK) {
tablesWithAutoPk.add(nextEntity);
}
}
}
// sort table list
if (tables.size() > 1) {
Collections.sort(tables, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 == null) {
return 1;
} else if (o2 == null) {
return -1;
} else if (!(o1 instanceof DbEntity)) {
return 1;
} else if (!(o2 instanceof DbEntity)) {
return -1;
} else {
return ((DbEntity) o1).getName().compareTo(((DbEntity) o2).getName());
}
}
});
if (this.dbEntityComparator != null) {
Vector<Object> v = new Vector<Object>();
v.addAll(tables);
tables.clear();
Iterator i = v.iterator();
while (i.hasNext()) {
DbEntity e = (DbEntity) i.next();
if (tables.size() == 0) {
tables.add(e);
} else {
Iterator i1 = tables.iterator();
int j = 0;
boolean inserted = false;
while (!inserted && i1.hasNext()) {
DbEntity e1 = (DbEntity) i1.next();
if (dbEntityComparator.compare(e, e1) < 0) {
tables.insertElementAt(e, j);
inserted = true;
}
j++;
}
if (!inserted) {
tables.add(e);
}
}
}
} else {
DataNode node = new DataNode("temp");
node.addDataMap(map);
node.getEntitySorter().sortDbEntities(tables, false);
}
}
this.dbEntitiesInInsertOrder = tables;
this.dbEntitiesRequiringAutoPK = tablesWithAutoPk;
}
}