/*****************************************************************
* 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.
****************************************************************/
package org.apache.cayenne.unit.di.server;
import org.apache.cayenne.access.DataDomain;
import org.apache.cayenne.access.DataNode;
import org.apache.cayenne.access.DbGenerator;
import org.apache.cayenne.access.dbsync.SkipSchemaUpdateStrategy;
import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory;
import org.apache.cayenne.access.translator.batch.DefaultBatchTranslatorFactory;
import org.apache.cayenne.access.translator.select.DefaultSelectTranslatorFactory;
import org.apache.cayenne.ashwood.AshwoodEntitySorter;
import org.apache.cayenne.cache.MapQueryCache;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.event.DefaultEventManager;
import org.apache.cayenne.log.JdbcEventLogger;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.MapLoader;
import org.apache.cayenne.map.Procedure;
import org.apache.cayenne.testdo.extended_type.StringET1ExtendedType;
import org.apache.cayenne.unit.UnitDbAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
/**
* Default implementation of the AccessStack that has a single DataNode per
* DataMap.
*/
public class SchemaBuilder {
private static Logger logger = LoggerFactory.getLogger(SchemaBuilder.class);
public static final String SKIP_SCHEMA_KEY = "cayenneTestSkipSchemaCreation";
private static String[] MAPS_REQUIRING_SCHEMA_SETUP = { "testmap.map.xml", "compound.map.xml",
"misc-types.map.xml", "things.map.xml", "numeric-types.map.xml", "binary-pk.map.xml", "no-pk.map.xml",
"lob.map.xml", "date-time.map.xml", "enum.map.xml", "extended-type.map.xml", "generated.map.xml",
"mixed-persistence-strategy.map.xml", "people.map.xml", "primitive.map.xml", "inheritance.map.xml",
"locking.map.xml", "soft-delete.map.xml", "empty.map.xml", "relationships.map.xml",
"relationships-activity.map.xml", "relationships-delete-rules.map.xml",
"relationships-collection-to-many.map.xml", "relationships-child-master.map.xml",
"relationships-clob.map.xml", "relationships-flattened.map.xml", "relationships-set-to-many.map.xml",
"relationships-to-many-fk.map.xml", "relationships-to-one-fk.map.xml", "return-types.map.xml",
"uuid.map.xml", "multi-tier.map.xml", "reflexive.map.xml", "delete-rules.map.xml",
"lifecycle-callbacks-order.map.xml", "lifecycles.map.xml", "map-to-many.map.xml", "toone.map.xml", "meaningful-pk.map.xml",
"table-primitives.map.xml", "generic.map.xml", "map-db1.map.xml", "map-db2.map.xml", "embeddable.map.xml",
"qualified.map.xml", "quoted-identifiers.map.xml", "inheritance-single-table1.map.xml",
"inheritance-vertical.map.xml", "oneway-rels.map.xml", "unsupported-distinct-types.map.xml",
"array-type.map.xml", "cay-2032.map.xml", "weighted-sort.map.xml" };
// hardcoded dependent entities that should be excluded
// if LOBs are not supported
private static final String[] EXTRA_EXCLUDED_FOR_NO_LOB = new String[] { "CLOB_DETAIL" };
private ServerCaseDataSourceFactory dataSourceFactory;
private UnitDbAdapter unitDbAdapter;
private DbAdapter dbAdapter;
private DataDomain domain;
private JdbcEventLogger jdbcEventLogger;
public SchemaBuilder(@Inject ServerCaseDataSourceFactory dataSourceFactory, @Inject UnitDbAdapter unitDbAdapter,
@Inject DbAdapter dbAdapter, @Inject JdbcEventLogger jdbcEventLogger) {
this.dataSourceFactory = dataSourceFactory;
this.unitDbAdapter = unitDbAdapter;
this.dbAdapter = dbAdapter;
this.jdbcEventLogger = jdbcEventLogger;
}
/**
* Completely rebuilds test schema.
*/
// TODO - this method changes the internal state of the object ... refactor
public void rebuildSchema() {
// generate schema combining all DataMaps that require schema support.
// Schema generation is done like that instead of per DataMap on demand
// to avoid conflicts when dropping and generating PK objects.
DataMap[] maps = new DataMap[MAPS_REQUIRING_SCHEMA_SETUP.length];
for (int i = 0; i < maps.length; i++) {
InputStream stream = getClass().getClassLoader().getResourceAsStream(MAPS_REQUIRING_SCHEMA_SETUP[i]);
InputSource in = new InputSource(stream);
in.setSystemId(MAPS_REQUIRING_SCHEMA_SETUP[i]);
maps[i] = new MapLoader().loadDataMap(in);
}
this.domain = new DataDomain("temp");
domain.setEventManager(new DefaultEventManager(2));
domain.setEntitySorter(new AshwoodEntitySorter());
domain.setQueryCache(new MapQueryCache(50));
try {
for (DataMap map : maps) {
initNode(map);
}
if ("true".equalsIgnoreCase(System.getProperty(SKIP_SCHEMA_KEY))) {
logger.info("skipping schema generation... ");
} else {
dropSchema();
dropPKSupport();
createSchema();
createPKSupport();
}
} catch (Exception e) {
throw new RuntimeException("Error rebuilding schema", e);
}
}
private void initNode(DataMap map) throws Exception {
DataNode node = new DataNode(map.getName());
node.setJdbcEventLogger(jdbcEventLogger);
node.setAdapter(dbAdapter);
node.setDataSource(dataSourceFactory.getSharedDataSource());
// setup test extended types
node.getAdapter().getExtendedTypes().registerType(new StringET1ExtendedType());
// tweak mapping with a delegate
for (Procedure proc : map.getProcedures()) {
unitDbAdapter.tweakProcedure(proc);
}
filterDataMap(map);
node.addDataMap(map);
node.setSchemaUpdateStrategy(new SkipSchemaUpdateStrategy());
node.setRowReaderFactory(new DefaultRowReaderFactory());
node.setBatchTranslatorFactory(new DefaultBatchTranslatorFactory());
node.setSelectTranslatorFactory(new DefaultSelectTranslatorFactory());
domain.addNode(node);
}
/**
* Remote binary pk {@link DbEntity} for {@link DbAdapter} not supporting
* that and so on.
*/
protected void filterDataMap(DataMap map) {
boolean supportsBinaryPK = unitDbAdapter.supportsBinaryPK();
if (supportsBinaryPK) {
return;
}
List<DbEntity> entitiesToRemove = new ArrayList<DbEntity>();
for (DbEntity ent : map.getDbEntities()) {
for (DbAttribute attr : ent.getAttributes()) {
// check for BIN PK or FK to BIN Pk
if (attr.getType() == Types.BINARY || attr.getType() == Types.VARBINARY
|| attr.getType() == Types.LONGVARBINARY) {
if (attr.isPrimaryKey() || attr.isForeignKey()) {
entitiesToRemove.add(ent);
break;
}
}
}
}
for (DbEntity e : entitiesToRemove) {
map.removeDbEntity(e.getName(), true);
}
}
/** Drops all test tables. */
private void dropSchema() throws Exception {
for (DataNode node : domain.getDataNodes()) {
dropSchema(node, node.getDataMaps().iterator().next());
}
}
/**
* Creates all test tables in the database.
*/
private void createSchema() throws Exception {
for (DataNode node : domain.getDataNodes()) {
createSchema(node, node.getDataMaps().iterator().next());
}
}
public void dropPKSupport() throws Exception {
for (DataNode node : domain.getDataNodes()) {
dropPKSupport(node, node.getDataMaps().iterator().next());
}
}
/**
* Creates primary key support for all node DbEntities. Will use its
* facilities provided by DbAdapter to generate any necessary database
* objects and data for primary key support.
*/
public void createPKSupport() throws Exception {
for (DataNode node : domain.getDataNodes()) {
createPKSupport(node, node.getDataMaps().iterator().next());
}
}
/**
* Helper method that orders DbEntities to satisfy referential constraints
* and returns an ordered list.
*/
private List<DbEntity> dbEntitiesInInsertOrder(DataNode node, DataMap map) {
List<DbEntity> entities = new ArrayList<DbEntity>(map.getDbEntities());
dbEntitiesFilter(entities);
domain.getEntitySorter().sortDbEntities(entities, false);
return entities;
}
protected List<DbEntity> dbEntitiesInDeleteOrder(DataMap dataMap) {
DataMap map = domain.getDataMap(dataMap.getName());
List<DbEntity> entities = new ArrayList<>(map.getDbEntities());
dbEntitiesFilter(entities);
domain.getEntitySorter().sortDbEntities(entities, true);
return entities;
}
private void dbEntitiesFilter(List<DbEntity> entities) {
// filter various unsupported tests...
// LOBs
boolean excludeLOB = !unitDbAdapter.supportsLobs();
boolean excludeBinPK = !unitDbAdapter.supportsBinaryPK();
if (excludeLOB || excludeBinPK) {
List<DbEntity> filtered = new ArrayList<DbEntity>();
for (DbEntity ent : entities) {
// check for LOB attributes
if (excludeLOB) {
if (Arrays.binarySearch(EXTRA_EXCLUDED_FOR_NO_LOB, ent.getName()) >= 0) {
continue;
}
boolean hasLob = false;
for (final DbAttribute attr : ent.getAttributes()) {
if (attr.getType() == Types.BLOB || attr.getType() == Types.CLOB) {
hasLob = true;
break;
}
}
if (hasLob) {
continue;
}
}
// check for BIN PK
if (excludeBinPK) {
boolean skip = false;
for (final DbAttribute attr : ent.getAttributes()) {
// check for BIN PK or FK to BIN Pk
if (attr.getType() == Types.BINARY || attr.getType() == Types.VARBINARY
|| attr.getType() == Types.LONGVARBINARY) {
if (attr.isPrimaryKey() || attr.isForeignKey()) {
skip = true;
break;
}
}
}
if (skip) {
continue;
}
}
filtered.add(ent);
}
entities = filtered;
}
}
private void dropSchema(DataNode node, DataMap map) throws Exception {
List<DbEntity> list = dbEntitiesInInsertOrder(node, map);
try (Connection conn = dataSourceFactory.getSharedDataSource().getConnection();) {
DatabaseMetaData md = conn.getMetaData();
List<String> allTables = new ArrayList<String>();
try (ResultSet tables = md.getTables(null, null, "%", null)) {
while (tables.next()) {
// 'toUpperCase' is needed since most databases
// are case insensitive, and some will convert names to
// lower
// case
// (PostgreSQL)
String name = tables.getString("TABLE_NAME");
if (name != null)
allTables.add(name.toUpperCase());
}
}
unitDbAdapter.willDropTables(conn, map, allTables);
// drop all tables in the map
try (Statement stmt = conn.createStatement();) {
ListIterator<DbEntity> it = list.listIterator(list.size());
while (it.hasPrevious()) {
DbEntity ent = it.previous();
if (!allTables.contains(ent.getName().toUpperCase())) {
continue;
}
for (String dropSql : node.getAdapter().dropTableStatements(ent)) {
try {
logger.info(dropSql);
stmt.execute(dropSql);
} catch (SQLException sqe) {
logger.warn("Can't drop table " + ent.getName() + ", ignoring...", sqe);
}
}
}
}
unitDbAdapter.droppedTables(conn, map);
}
}
private void dropPKSupport(DataNode node, DataMap map) throws Exception {
List<DbEntity> filteredEntities = dbEntitiesInInsertOrder(node, map);
node.getAdapter().getPkGenerator().dropAutoPk(node, filteredEntities);
}
private void createPKSupport(DataNode node, DataMap map) throws Exception {
List<DbEntity> filteredEntities = dbEntitiesInInsertOrder(node, map);
node.getAdapter().getPkGenerator().createAutoPk(node, filteredEntities);
}
private void createSchema(DataNode node, DataMap map) throws Exception {
try (Connection conn = dataSourceFactory.getSharedDataSource().getConnection();) {
unitDbAdapter.willCreateTables(conn, map);
try (Statement stmt = conn.createStatement();) {
for (String query : tableCreateQueries(node, map)) {
logger.info(query);
stmt.execute(query);
}
}
unitDbAdapter.createdTables(conn, map);
}
}
/**
* Returns iterator of preprocessed table create queries.
*/
private Collection<String> tableCreateQueries(DataNode node, DataMap map) throws Exception {
DbAdapter adapter = node.getAdapter();
DbGenerator gen = new DbGenerator(adapter, map, null, domain, jdbcEventLogger);
List<DbEntity> orderedEnts = dbEntitiesInInsertOrder(node, map);
List<String> queries = new ArrayList<String>();
// table definitions
for (DbEntity ent : orderedEnts) {
queries.add(adapter.createTable(ent));
}
// FK constraints
for (DbEntity ent : orderedEnts) {
if (!unitDbAdapter.supportsFKConstraints(ent)) {
continue;
}
List<String> qs = gen.createConstraintsQueries(ent);
queries.addAll(qs);
}
return queries;
}
}