All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ch.inftec.ju.testing.db.DbSchemaUtil Maven / Gradle / Ivy

package ch.inftec.ju.testing.db;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.sql.Connection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.sql.DataSource;
import javax.transaction.UserTransaction;

import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.inftec.ju.db.DbWork;
import ch.inftec.ju.db.DsWork;
import ch.inftec.ju.db.JuConnUtil;
import ch.inftec.ju.db.JuConnUtil.DbType;
import ch.inftec.ju.db.JuEmUtil;
import ch.inftec.ju.testing.db.DbDataUtil.ImportBuilder;
import ch.inftec.ju.util.AssertUtil;
import ch.inftec.ju.util.IOUtil;
import ch.inftec.ju.util.JuRuntimeException;

import com.googlecode.flyway.core.Flyway;

/**
 * Util class containing methods to perform DB Schema actions. Uses an EntityManager
 * to establish the connection to the corresponding DB.
 * @author Martin
 *
 */
public class DbSchemaUtil {
	private final Logger logger = LoggerFactory.getLogger(DbSchemaUtil.class);
	
	/**
	 * Optional utility instance to run liquibase updates. May be used if transaction handling requires it. 
	 */
	private final JuConnUtil connUtil;
	
	public DbSchemaUtil(JuConnUtil connUtil) {
		this.connUtil = connUtil;
	}
	
	public DbSchemaUtil(JuEmUtil emUtil) {
		this(emUtil.asConnUtil());
	}
	
	/**
	 * Creating a SchemaUtil with a JuEmUtil to get Database meta info and a JuConnUtil to perform
	 * the update or cleaning of the Schema.
	 * 

* JuEmUtil must be within a valid transaction - no explicit transaction handling will be done. * @param emUtil * @param connUtil */ // public DbSchemaUtil(JuEmUtil emUtil, JuConnUtil2 connUtil) { // this(emUtil, TxHandler.getDummyHandler(), connUtil); // } // public DbSchemaUtil(EntityManager em) { this(new JuEmUtil(em)); } /** * Initializes the DbSchemaUtil with an EntityManager and a UserTransaction object. *

* This initialization must be used in a container environment in a bean * managed transaction context. Otherwise, we cannot control the transactions the way * we have to. Liquibase uses its own transaction management and this is not possible if there * is a managed transaction running. On the other hand, we perform some operations on the * EntityManager that require a transaction to be active. * @param em EntityManager provided by the container * @param tx UserTransaction instance of a bean managed transaction context */ @Deprecated public DbSchemaUtil(EntityManager em, UserTransaction tx) { this(new JuEmUtil(em)); } /** * Runs the specified liquibase change log. *

* Liquibase uses a JDBC datasource to get a connection to the database and has its own transaction handling. * Therefore, it won't participate in the current transaction. *

* We'll still require a transaction to be present for the current EntityManager. We'll commit that transaction * in order to execute the Liquibase changes and begin a new transaction before returning. * That means that the EntityManager's transaction will be open at the end of this method and can be used * to perform further queries / updates. * @param changeLogResourceName Name of the change log resource. The resource will be loaded using the * default class loader. */ public void runLiquibaseChangeLog(final String changeLogResourceName) { this.runLiquibaseChangeLog(changeLogResourceName, null); } /** * Method to actually run the Liquibase ChangeLog. *

* TODO: * * @param changeLogResourceName * @param parameters */ private void runLiquibaseChangeLog(final String changeLogResourceName, final Map parameters) { // Make sure we have a transaction when accessing entity manager meta data final DbType dbType = this.connUtil.getDbType(); final String metaDataUserName = this.connUtil.getMetaDataInfo().getUserName(); this.connUtil.doWork(new DbWork() { @Override public void execute(Connection conn) { try { JdbcConnection jdbcConn = new JdbcConnection(conn); /* * The default implementation of Liquibase for Oracle has an error in the default Schema * lookup, so we'll set it here to avoid problems. */ Database db = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConn); if (dbType == DbType.ORACLE) { db.setDefaultSchemaName(metaDataUserName); } /* * Derby (and others) don't support the CREATE OR REPLACE syntax for Views and Liquibase will throw * an error if the attribute is specified for Derby or H2. * As we will use those DBs usually in memory, we'll just remote the attribute in all change logs * using a custom ResourceAccessor that will filter the character stream. */ ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(); if (dbType == DbType.DERBY || dbType == DbType.H2 || dbType == DbType.HSQL) { resourceAccessor = new ResourceAccessorFilter(resourceAccessor); } Liquibase liquibase = new Liquibase(changeLogResourceName, resourceAccessor, db); // Set parameters (if any) if (parameters != null) { for (String key : parameters.keySet()) { liquibase.setChangeLogParameter(key, parameters.get(key)); } } // Run update liquibase.update((String)null); } catch (Exception ex) { throw new JuRuntimeException("Couldn't run Liquibase Update %s", ex, changeLogResourceName); } } }); } /** * Returns a builder to set up a Liquibase ChangeLog run. *

* Allows to set liquibase parameters as well. * * @return */ public LiquibaseChangeLogBuilder liquibaseChangeLog() { return new LiquibaseChangeLogBuilder(); } /** * Helper class to run Liquibase ChangeLogs in a fluent API way. * * @author Martin Meyer * */ public class LiquibaseChangeLogBuilder { private String resourcePath; private final Map parameters = new HashMap<>(); /** * Sets the Liquibase ChangeLog resource path. * * @param resourcePath * @return */ public LiquibaseChangeLogBuilder changeLogResource(String resourcePath) { AssertUtil.assertNull("Multiple resources not supported yet.", this.resourcePath); this.resourcePath = resourcePath; return this; } /** * Sets a key/value pair for Liquibase parameter substitution (parameters like ${key} in * changeLogs will be replaced by the corresponding value. * * @param name * Parameter name * @param value * Substitution value * @return */ public LiquibaseChangeLogBuilder parameter(String name, String value) { this.parameters.put(name, value); return this; } public void run() { runLiquibaseChangeLog(resourcePath, parameters); } } /** * Runs Flyway migration scripts. * @param locations Locations containing scripts in Flyway structure (e.g. db/migration). */ public void runFlywayMigration(final String... locations) { this.connUtil.doWork(new DsWork() { @Override public void execute(DataSource ds) { Flyway flyway = new Flyway(); flyway.setDataSource(ds); flyway.setLocations(locations); flyway.migrate(); } }); } /** * Clears the DB Schema. *

* Uses Flyway functionality. */ public void clearSchema() { this.connUtil.doWork(new DsWork() { @Override public void execute(DataSource ds) { Flyway flyway = new Flyway(); flyway.setDataSource(ds); flyway.clean(); // FIXME: Try Liquibase.dropAll() } }); } /** * Creates the Default test DB Schema (Player, Team, TestingEntity...) and * loads the default test data. *

* Also resets the sequences to 1. */ public void prepareDefaultSchemaAndTestData() { this.prepareDefaultTestData(false, true, true); } /** * Loads the default test data and resets the sequences to 1. *

* Doesn't perform Schema updates. */ public void loadDefaultTestData() { this.prepareDefaultTestData(false, true, false); } /** * Loads the default test data (Player, Team, TestingEntity, ...), making * sure that the tables have been created using Liquibase. *

* This method will use Liquibase to perform Schema updates. Therefore, we'll need a transaction when the method is * started that we will have to commit in order to execute Liquibase. We'll make sure however that we start a new * transaction before returning. * @param emptyTables If true, the default tables will be cleaned * @param resetSequences If true, sequences (or identity columns) will be reset to 1 * @param createSchema If true, the Schema will be created (or verified) using Liquibase */ public void prepareDefaultTestData(boolean emptyTables, boolean resetSequences, boolean createSchema) { DbType dbType = this.connUtil.getDbType(); if (createSchema) { this.runLiquibaseChangeLog("ju-testing/data/default-changeLog.xml"); // For non-MySQL DBs, we also need to create the hibernate_sequence sequence... if (dbType != DbType.MYSQL) { this.runLiquibaseChangeLog("ju-testing/data/default-changeLog-hibernateSequence.xml"); } // For MySQL DBs, we need to change the engine to support transactions, otherwise transaction tests // will fail if (dbType == DbType.MYSQL) { this.runLiquibaseChangeLog("ju-testing/data/default-changeLog-mySqlEngine.xml"); } } DbDataUtil du = new DbDataUtil(this.connUtil); ImportBuilder fullData = du.buildImport().from("/ju-testing/data/default-fullData.xml"); if (emptyTables) { fullData.executeDeleteAll(); } else { fullData.executeCleanInsert(); } // Load TIMEFIELD for non-oracle DBs if (this.connUtil.getDbType() != DbType.ORACLE && !emptyTables) { du.buildImport().from("/ju-testing/data/default-fullData-dataTypes.xml").executeUpdate(); } if (resetSequences) { this.connUtil.getDbHandler().resetIdentityGenerationOrSequences(1); } } private class ResourceAccessorFilter implements ResourceAccessor { private final ResourceAccessor accessor; private ResourceAccessorFilter(ResourceAccessor accessor) { this.accessor = accessor; } @Override public InputStream getResourceAsStream(String file) throws IOException { logger.debug("Removing replaceIfExists attribute for resource " + file); InputStream is = this.accessor.getResourceAsStream(file); InputStreamReader reader = new InputStreamReader(is, "UTF-8"); String text = IOUtil.toString(reader); String newText = text.replaceAll("replaceIfExists=\"true\"", ""); return new BufferedInputStream(new ByteArrayInputStream(newText.getBytes())); } @Override public Enumeration getResources(String packageName) throws IOException { return this.accessor.getResources(packageName); } @Override public ClassLoader toClassLoader() { return this.accessor.toClassLoader(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy