/***************************************************************** * 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.dbsync.merge; import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.access.DataNode; import org.apache.cayenne.configuration.server.ServerRuntime; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.dba.TypesMapping; import org.apache.cayenne.dbsync.merge.context.MergerContext; import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory; import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactoryProvider; import org.apache.cayenne.dbsync.merge.token.db.AbstractToDbToken; import org.apache.cayenne.dbsync.merge.token.MergerToken; import org.apache.cayenne.dbsync.merge.token.db.SetColumnTypeToDb; import org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator; import org.apache.cayenne.dbsync.naming.NoStemStemmer; import org.apache.cayenne.dbsync.reverse.dbload.DbLoader; import org.apache.cayenne.dbsync.reverse.dbload.DbLoaderConfiguration; import org.apache.cayenne.dbsync.reverse.dbload.LoggingDbLoaderDelegate; import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig; import org.apache.cayenne.dbsync.reverse.filters.PatternFilter; import org.apache.cayenne.dbsync.reverse.filters.TableFilter; import org.apache.cayenne.dbsync.unit.DbSyncCase; import org.apache.cayenne.di.Inject; import org.apache.cayenne.map.DataMap; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.test.jdbc.DBHelper; import org.apache.cayenne.unit.UnitDbAdapter; import org.apache.cayenne.unit.di.server.CayenneProjects; import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory; import org.apache.cayenne.unit.di.server.UseServerRuntime; import org.slf4j.Logger; import org.junit.Before; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) public abstract class MergeCase extends DbSyncCase { @Inject protected EntityResolver resolver; @Inject protected DataNode node; protected DataMap map; private Logger logger = LoggerFactory.getLogger(MergeCase.class); @Inject private DBHelper dbHelper; @Inject private ServerRuntime runtime; @Inject protected UnitDbAdapter accessStackAdapter; @Inject private ServerCaseDataSourceFactory dataSourceFactory; @Override public void cleanUpDB() throws Exception { dbHelper.update("ARTGROUP").set("PARENT_GROUP_ID", null, Types.INTEGER).execute(); super.cleanUpDB(); } @Before public void setUp() throws Exception { // this map can't be safely modified in this test, as it is reset by DI // container // on every test map = runtime.getDataDomain().getDataMap("testmap"); filterDataMap(); List<MergerToken> tokens = createMergeTokens(); execute(tokens); assertTokensAndExecute(0, 0); } protected DataMapMerger.Builder merger() { return DataMapMerger.builder(mergerFactory()); } protected List<MergerToken> createMergeTokens() { return createMergeTokens("ARTIST|GALLERY|PAINTING|NEW_TABLE2?"); } protected List<MergerToken> createMergeTokens(String tableFilterInclude) { FiltersConfig filters = FiltersConfig.create(null, null, TableFilter.include(tableFilterInclude), PatternFilter.INCLUDE_NOTHING); DbLoaderConfiguration loaderConfiguration = new DbLoaderConfiguration(); loaderConfiguration.setFiltersConfig(filters); DataMap dbImport; try (Connection conn = node.getDataSource().getConnection();) { dbImport = new DbLoader(node.getAdapter(), conn, loaderConfiguration, new LoggingDbLoaderDelegate(LoggerFactory.getLogger(DbLoader.class)), new DefaultObjectNameGenerator(NoStemStemmer.getInstance())) .load(); } catch (SQLException e) { throw new CayenneRuntimeException("Can't doLoad dataMap from db.", e); } List<MergerToken> tokens = merger().filters(filters).build().createMergeTokens(map, dbImport); return filter(tokens); } private List<MergerToken> filter(List<MergerToken> tokens) { return filterEmptyTypeChange(filterEmpty(tokens)); } /** * Filter out tokens for db attribute type change when types is same for specific DB */ private List<MergerToken> filterEmptyTypeChange(List<MergerToken> tokens) { List<MergerToken> tokensOut = new ArrayList<>(); for(MergerToken token : tokens) { if(!(token instanceof SetColumnTypeToDb)) { tokensOut.add(token); continue; } SetColumnTypeToDb setColumnToDb = (SetColumnTypeToDb)token; int toType = setColumnToDb.getColumnNew().getType(); int fromType = setColumnToDb.getColumnOriginal().getType(); // filter out conversions between date/time types if(accessStackAdapter.onlyGenericDateType()) { if(isDateTimeType(toType) && isDateTimeType(fromType)){ continue; } } // filter out conversions between numeric types if(accessStackAdapter.onlyGenericNumberType()) { if(TypesMapping.isNumeric(toType) && TypesMapping.isNumeric(fromType)) { continue; } } tokensOut.add(token); } return tokensOut; } private static boolean isDateTimeType(int type) { return type == Types.DATE || type == Types.TIME || type == Types.TIMESTAMP; } private List<MergerToken> filterEmpty(List<MergerToken> tokens) { List<MergerToken> tokensOut = new ArrayList<>(); for(MergerToken token : tokens) { if(!token.isEmpty()) { tokensOut.add(token); } } return tokensOut; } /** * Remote binary pk {@link DbEntity} for {@link DbAdapter} not supporting * that and so on. */ private void filterDataMap() { // copied from AbstractAccessStack.dbEntitiesInInsertOrder boolean excludeBinPK = accessStackAdapter.supportsBinaryPK(); if (!excludeBinPK) { 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); } } protected void execute(List<MergerToken> tokens) { MergerContext mergerContext = MergerContext.builder(map).dataNode(node).build(); for (MergerToken tok : tokens) { tok.execute(mergerContext); } } protected void execute(MergerToken token) throws Exception { MergerContext mergerContext = MergerContext.builder(map).dataNode(node).build(); token.execute(mergerContext); } private void executeSql(String sql) throws Exception { try (Connection conn = dataSourceFactory.getSharedDataSource().getConnection();) { try (Statement st = conn.createStatement();) { st.execute(sql); } } } protected void assertTokens(List<MergerToken> tokens, int expectedToDb, int expectedToModel) { int actualToDb = 0; int actualToModel = 0; for (MergerToken token : tokens) { if (token.getDirection().isToDb()) { actualToDb++; } else if (token.getDirection().isToModel()) { actualToModel++; } } assertEquals("tokens to db", expectedToDb, actualToDb); assertEquals("tokens to model", expectedToModel, actualToModel); } protected void assertTokensAndExecute(int expectedToDb, int expectedToModel) { List<MergerToken> tokens = createMergeTokens(); assertTokens(tokens, expectedToDb, expectedToModel); execute(tokens); } protected MergerTokenFactory mergerFactory() { return runtime.getInjector().getInstance(MergerTokenFactoryProvider.class).get(node.getAdapter()); } protected void dropTableIfPresent(String tableName) throws Exception { // must have a dummy datamap for the dummy table for the downstream code // to work DataMap map = new DataMap("dummy"); map.setQuotingSQLIdentifiers(map.isQuotingSQLIdentifiers()); DbEntity entity = new DbEntity(tableName); map.addDbEntity(entity); AbstractToDbToken t = (AbstractToDbToken) mergerFactory().createDropTableToDb(entity); for (String sql : t.createSql(node.getAdapter())) { try { executeSql(sql); } catch (Exception e) { logger.info("Exception dropping table " + tableName + ", probably abscent.."); } } } }