From a95c04173e122326009a24f423ad8c093cf7a3e1 Mon Sep 17 00:00:00 2001 From: erik Date: Sat, 3 Apr 2010 20:19:37 +0000 Subject: [PATCH] Preliminary version of JPA test support. Working now again. --- support/test/pom.xml | 80 ++++ .../support/jndi/StubInitialContext.java | 31 ++ .../jndi/StubInitialContextFactory.java | 63 +++ .../support/persistence/AbstractDatabase.java | 78 ++++ .../persistence/AbstractDatabaseProvider.java | 18 + .../wamblee/support/persistence/Database.java | 65 +++ .../support/persistence/DatabaseBuilder.java | 116 ++++++ .../persistence/DatabaseDescription.java | 41 ++ .../support/persistence/DatabaseProvider.java | 42 ++ .../support/persistence/DatabaseUtils.java | 394 ++++++++++++++++++ .../support/persistence/DerbyDatabase.java | 314 ++++++++++++++ .../persistence/DerbyDatabaseProvider.java | 32 ++ .../support/persistence/ExternalDatabase.java | 82 ++++ .../persistence/ExternalDatabaseProvider.java | 35 ++ .../support/persistence/JpaBuilder.java | 134 ++++++ .../support/persistence/JpaTester.java | 113 +++++ .../PersistenceUnitDescription.java | 40 ++ .../support/persistence/ToplinkTables.java | 20 + .../support/persistence/package-info.java | 23 + .../toplink/JndiSessionCustomizer.java | 59 +++ ...mblee.support.persistence.DatabaseProvider | 2 + .../jndi/StubInitiaContextFactoryTest.java | 41 ++ .../persistence/DatabaseBuilderTest.java | 16 + .../persistence/DatabaseUtilsTest.java | 89 ++++ .../persistence/DerbyDatabaseTest.java | 47 +++ .../persistence/ExternalDatabaseTest.java | 53 +++ .../wamblee/support/persistence/MyEntity.java | 39 ++ .../persistence/MyEntityExampleTest.java | 87 ++++ .../persistence/MyPersistenceUnit.java | 15 + .../wamblee/support/persistence/MyTables.java | 25 ++ .../test/resources/META-INF/persistence.xml | 40 ++ 31 files changed, 2234 insertions(+) create mode 100644 support/test/pom.xml create mode 100644 support/test/src/main/java/org/wamblee/support/jndi/StubInitialContext.java create mode 100644 support/test/src/main/java/org/wamblee/support/jndi/StubInitialContextFactory.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabase.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabaseProvider.java create mode 100755 support/test/src/main/java/org/wamblee/support/persistence/Database.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/DatabaseBuilder.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/DatabaseDescription.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/DatabaseProvider.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/DatabaseUtils.java create mode 100755 support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabase.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabaseProvider.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabase.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabaseProvider.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/JpaBuilder.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/JpaTester.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/PersistenceUnitDescription.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/ToplinkTables.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/package-info.java create mode 100644 support/test/src/main/java/org/wamblee/support/persistence/toplink/JndiSessionCustomizer.java create mode 100644 support/test/src/main/resources/META-INF/services/org.wamblee.support.persistence.DatabaseProvider create mode 100644 support/test/src/test/java/org/wamblee/support/jndi/StubInitiaContextFactoryTest.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/DatabaseBuilderTest.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/DatabaseUtilsTest.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/DerbyDatabaseTest.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/ExternalDatabaseTest.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/MyEntity.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/MyEntityExampleTest.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/MyPersistenceUnit.java create mode 100644 support/test/src/test/java/org/wamblee/support/persistence/MyTables.java create mode 100644 support/test/src/test/resources/META-INF/persistence.xml diff --git a/support/test/pom.xml b/support/test/pom.xml new file mode 100644 index 00000000..5ab92ab2 --- /dev/null +++ b/support/test/pom.xml @@ -0,0 +1,80 @@ + + + + org.wamblee + wamblee-utils + 0.2-SNAPSHOT + + + 4.0.0 + org.wamblee + wamblee-support-test + jar + wamblee.org support general library + http://wamblee.org + + + + org.wamblee + wamblee-support-general + 0.2-SNAPSHOT + test-jar + + + javax.transaction + transaction-api + + + junit + junit + compile + + + org.apache.derby + derby + + + mysql + mysql-connector-java + + + org.apache.derby + derbyclient + + + org.apache.derby + derbynet + + + commons-dbcp + commons-dbcp + + + toplink.essentials + toplink-essentials + + + javax.persistence + persistence-api + + + toplink.essentials + toplink-essentials + + + org.dbunit + dbunit + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-jdk14 + + + + diff --git a/support/test/src/main/java/org/wamblee/support/jndi/StubInitialContext.java b/support/test/src/main/java/org/wamblee/support/jndi/StubInitialContext.java new file mode 100644 index 00000000..a7e4c965 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/jndi/StubInitialContext.java @@ -0,0 +1,31 @@ +package org.wamblee.support.jndi; + +import java.util.HashMap; +import java.util.Map; + +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; + +public class StubInitialContext extends InitialContext { + private Map bindings = new HashMap(); + + public StubInitialContext() throws NamingException { + super(true); + } + + @Override + public void bind(String name, Object obj) throws NamingException { + bindings.put(name, obj); + } + + @Override + public Object lookup(String name) throws NamingException { + return bindings.get(name); + } + + @Override + public Object lookup(Name name) throws NamingException { + return super.lookup(name.toString()); + } +} diff --git a/support/test/src/main/java/org/wamblee/support/jndi/StubInitialContextFactory.java b/support/test/src/main/java/org/wamblee/support/jndi/StubInitialContextFactory.java new file mode 100644 index 00000000..c7e4e2fd --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/jndi/StubInitialContextFactory.java @@ -0,0 +1,63 @@ +package org.wamblee.support.jndi; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; + +/** + * Test initial context factory used for testing software in a Java SE + * environnment that uses JNDI to retrieve objects. + * + * See {@link #bind(String, Object)} to resp. register the initial context. + * + * To bind objects in the JNDI tree simply use the standard JNDI api: + * InitialContext context = new InitialContext(); + * MyClass myObj = ...; + * context.bind("a/b", myObj); + * + */ +public class StubInitialContextFactory implements InitialContextFactory { + + private static Context context; + + private static void initialize() { + try { + context = new StubInitialContext(); + } catch (NamingException e) { // can't happen. + throw new RuntimeException(e); + } + } + + /** + * This method must be called to register this initial context factory as + * the default implementation for JNDI. + * + * @throws Exception + */ + public static void register() { + // sets up the InitialContextFactoryForTest as default factory. + System.setProperty(Context.INITIAL_CONTEXT_FACTORY, + StubInitialContextFactory.class.getName()); + if (context == null) { + initialize(); + } + } + + /** + * Unregisters the initial context factory + */ + public static void unregister() { + System.setProperty(Context.INITIAL_CONTEXT_FACTORY, ""); + context = null; + } + + public Context getInitialContext(Hashtable environment) + throws NamingException { + return context; + } +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabase.java b/support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabase.java new file mode 100644 index 00000000..59f3d776 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabase.java @@ -0,0 +1,78 @@ +package org.wamblee.support.persistence; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp.ConnectionFactory; +import org.apache.commons.dbcp.DriverManagerConnectionFactory; +import org.apache.commons.dbcp.PoolableConnectionFactory; +import org.apache.commons.dbcp.PoolingDataSource; +import org.apache.commons.pool.impl.GenericObjectPool; + +public abstract class AbstractDatabase implements Database { + private static final int CONNECTION_POOL_SIZE = 16; + + private DataSource itsDataSource; + + private boolean started; + + protected AbstractDatabase() { + started = false; + } + + protected abstract void doStart(); + + protected abstract void doStop(); + + /** + * This method must be called from the start method. + */ + protected final void createDataSource() { + GenericObjectPool connectionPool = new GenericObjectPool(null); + connectionPool.setMaxActive(CONNECTION_POOL_SIZE); + ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(getJdbcUrl(), getUsername(), getPassword()); + // The following line must be kept in although it does not appear to be used, the constructor regsiters the + // constructed object at the connection pool. + PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory(connectionFactory,connectionPool,null,null,false,true); + itsDataSource = new PoolingDataSource(connectionPool); + } + + + /// BELOW THIS LINE IS NOT OF INTEREST TO SUBCLASSES. + + public final DataSource start() { + if ( started ) { + throw new RuntimeException("Database already started"); + } + started = true; + doStart(); + return getDatasource(); + } + + public final void stop() { + if ( ! started ) { + return; // nothing to do. + } + started = false; + doStop(); + } + + private final DataSource getDatasource() { + if ( !started) { + throw new RuntimeException("Database is not started!"); + } + return itsDataSource; + } + + protected String getProperty(String aName) { + String value = System.getProperty(aName); + if ( value != null ) { + return value; + } + value = System.getenv(aName); + if ( value != null ) { + return value; + } + throw new RuntimeException("This class expects the '" + aName + "' property to be set"); + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabaseProvider.java b/support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabaseProvider.java new file mode 100644 index 00000000..6ed9d6cb --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/AbstractDatabaseProvider.java @@ -0,0 +1,18 @@ +package org.wamblee.support.persistence; + +import java.util.List; + +public abstract class AbstractDatabaseProvider implements DatabaseProvider { + + protected abstract List getCapabilities(); + + public final boolean supportsCapabilities(String[] aCapabilities) { + for (String capability: aCapabilities) { + if ( !getCapabilities().contains(capability)) { + return false; + } + } + return true; + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/Database.java b/support/test/src/main/java/org/wamblee/support/persistence/Database.java new file mode 100755 index 00000000..56122ed1 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/Database.java @@ -0,0 +1,65 @@ +/* + * Copyright 2005 the original author or authors. + * + * 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 org.wamblee.support.persistence; + +import java.util.List; + +import javax.sql.DataSource; + +/** + * Represents a database. + * + * @author Erik Brakkee + */ +public interface Database { + + /** + * Starts a database. This call should not block and return as soon as the + * database has been started. + */ + DataSource start(); + + /** + * Gets the Jdbc Url to connect to this database. + * + * @return Jdbc Url. + */ + String getJdbcUrl(); + + /** + * Gets the external Jdbc URL to connect to this database from other JVMs. + */ + String getExternalJdbcUrl(); + + /** + * Gets the username to connect to the database. + * + * @return username. + */ + String getUsername(); + + /** + * Gets the password to connect to the database. + * + * @return password. + */ + String getPassword(); + + /** + * Stops a database. + */ + void stop(); +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/DatabaseBuilder.java b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseBuilder.java new file mode 100644 index 00000000..9c599a0a --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseBuilder.java @@ -0,0 +1,116 @@ +package org.wamblee.support.persistence; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import java.util.logging.Logger; + +/** + * DatabaseBuilder is used from unit test to obtain a reference to a database + * from unit test. This database is either an inmemory database or represents an + * external database. Purpose of this utility is to make test code independent of the + * particular database used and specifically to be able to run database tests without + * any configuration at all (using an inmemory database). + * + * The type of database to use can be overridden by specifying either a system + * property or an environment variable ({@link #DB_CAPABILITIES_PROP}) that + * contains the comma-separated capabilities of the database. Each database type + * provides its own capabilities (see {@link DatabaseProvider} implementations}. + * + * + * There are currently two database types available: + *
    + *
  • Derby: AN inmemory derby. Provided by {@link DerbyDatabaseProvider}. This is the default. + *
  • + *
  • External: An arbitrary external database configured using system properties or environment variables + * in the usual way using a JDBC URL, username, and password. + *
  • + *
+ * + * The DatabaseBuilder uses the {@link ServiceLoader} mechanism to find implementations + * of {@link DatabaseProvider} on the classpath. In the {@link #getDatabase(String...)} method a number of + * capabilities are passed. The database providers are then searched in (arbitrary) order and the first one that + * has all required capabilities is returned. + * + * {@link #getSupportedDatabases()} gives a list of all available databases. + */ +public class DatabaseBuilder { + + private static final Logger LOGGER = Logger.getLogger(DatabaseBuilder.class + .getName()); + + /** + * Environmment variable by which capabilities of the requested database can + * be defined + */ + private static final String DB_CAPABILITIES_PROP = "TEST_DB_CAPABILITIES"; + + private static ServiceLoader LOADER = null; + + private DatabaseBuilder() { + // Empty. + } + + private static String[] parseCapabilities(String aValue) { + return aValue.split(","); + } + + /** + * Gets the first database that has all required capabilities. + * @param aCapabilities Capabilities. + * @return Database to use. + */ + public static Database getDatabase(String... aCapabilities) { + if (aCapabilities.length == 0) { + LOGGER.info("Examining database capabilities"); + LOGGER.info(" Checking system property " + DB_CAPABILITIES_PROP); + String capabilities = System.getProperty(DB_CAPABILITIES_PROP); + if (capabilities != null) { + aCapabilities = parseCapabilities(capabilities); + } else { + LOGGER.info(" Checking environment variable " + + DB_CAPABILITIES_PROP); + capabilities = System.getenv(DB_CAPABILITIES_PROP); + if (capabilities != null) { + aCapabilities = parseCapabilities(capabilities); + } else { + LOGGER.info(" Using default capabilities"); + aCapabilities = new String[] { DatabaseProvider.CAPABILITY_IN_MEMORY }; + } + } + LOGGER.info("Using capabilities: " + aCapabilities); + } + synchronized (DatabaseBuilder.class) { + initLoader(); + for (DatabaseProvider db : LOADER) { + if (db.supportsCapabilities(aCapabilities)) { + return db.create(); + } + } + } + throw new RuntimeException( + "No database found that satisfies capabilities: " + + Arrays.asList(aCapabilities)); + } + + /** + * Gets a list of available databases. + * @return List of databases. + */ + public static List getSupportedDatabases() { + initLoader(); + List descriptions = new ArrayList(); + for (DatabaseProvider db : LOADER) { + descriptions.add(db.getDescription()); + } + return descriptions; + } + + private static void initLoader() { + if (LOADER == null) { + LOADER = ServiceLoader.load(DatabaseProvider.class); + } + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/DatabaseDescription.java b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseDescription.java new file mode 100644 index 00000000..c31427bc --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseDescription.java @@ -0,0 +1,41 @@ +package org.wamblee.support.persistence; + +/** + * Description of a specific database. + */ +public class DatabaseDescription { + + private String[] itsCapabilities; + private String itsDatabase; + private String itsOther; + + /** + * Constructs the description. + * @param aCapabilities List of all capabilities. + * @param aDatabase Database. + * @param aOther Other information. + */ + public DatabaseDescription(String[] aCapabilities, String aDatabase, String aOther) { + itsCapabilities = aCapabilities; + itsDatabase = aDatabase; + itsOther = aOther; + } + + @Override + public String toString() { + return "\n Database " + itsDatabase + + "\n Capabilities: " + printCapabilities() + + "\n Other info: " + itsOther; + } + + private String printCapabilities() { + String res = ""; + for (int i = 0; i < itsCapabilities.length; i++) { + res += "" + itsCapabilities[i]; + if ( i < itsCapabilities.length -1 ) { + res += ", "; + } + } + return res; + } +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/DatabaseProvider.java b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseProvider.java new file mode 100644 index 00000000..f6736aa1 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseProvider.java @@ -0,0 +1,42 @@ +package org.wamblee.support.persistence; + +/** + * Database provider. This database provider represents a particular type of + * database such as its capabilities and the ability to create an instance + * representing a running database. + * + * Since the {@link DatabaseBuilder} uses a first match algorithm and the order + * of databaseproviders is not guaranteed, it is recommended for each database + * provider to also provide a unique capability that no other database has. + */ +public interface DatabaseProvider { + + /** + * Capability that all databases that run inmemory have. + */ + String CAPABILITY_IN_MEMORY = "INMEMORY"; + + /** + * Capability that all databases that are external have. + */ + String CAPABILITY_EXTERNAL = "EXTERNAL"; + + /** + * Determines if the database has all capabilities that are requested. + * @param aCapabilities Capabilities it must ahve + * @return True if it has all capabilities. + */ + boolean supportsCapabilities(String[] aCapabilities); + + /** + * Gets the description for the database. + * @return Description. + */ + DatabaseDescription getDescription(); + + /** + * Creates a database instance that represents a running instance of that database. + * @return Database. + */ + Database create(); +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/DatabaseUtils.java b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseUtils.java new file mode 100644 index 00000000..781764be --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/DatabaseUtils.java @@ -0,0 +1,394 @@ +package org.wamblee.support.persistence; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import junit.framework.TestCase; + +import org.dbunit.DataSourceDatabaseTester; +import org.dbunit.DatabaseTestCase; +import org.dbunit.DatabaseUnitException; +import org.dbunit.IDatabaseTester; +import org.dbunit.database.DatabaseConnection; +import org.dbunit.database.DatabaseSequenceFilter; +import org.dbunit.database.IDatabaseConnection; +import org.dbunit.dataset.FilteredDataSet; +import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.filter.ITableFilter; +import org.dbunit.dataset.filter.ITableFilterSimple; +import org.dbunit.operation.DatabaseOperation; + +/** + * Database utilities is a simple support class for common tasks in working with + * databases. + */ +public class DatabaseUtils { + + public static interface TableSet { + boolean contains(String aTableName); + } + + public static interface JdbcUnitOfWork { + T execute(Connection aConnection) throws Exception; + } + + public static interface TableSetOperation { + void execute(String aTable) throws Exception; + } + + private static final Logger LOG = Logger.getLogger(DatabaseUtils.class + .getName()); + + /** + * Schema pattern. + */ + private static final String SCHEMA_PATTERN = "%"; + private DataSource dataSource; + private ITableFilterSimple tables; + + + public DatabaseUtils(DataSource aDataSource, ITableFilterSimple aTables) { + dataSource = aDataSource; + tables = aTables; + } + + public IDatabaseTester createDbTester() throws Exception { + return createDbTester(getTableNames(tables)); + } + + public IDatabaseTester createDbTester(String[] aTables) throws Exception { + IDatabaseTester dbtester = new DataSourceDatabaseTester(dataSource); + dbtester.setDataSet(dbtester.getConnection().createDataSet(aTables)); + return dbtester; + } + + public void cleanDatabase() throws Exception { + cleanDatabase(tables); + } + + public void executeOnTables(ITableFilterSimple aTables, + final TableSetOperation aOperation) throws Exception { + final String[] tables = getTableNames(aTables); + executeInTransaction(new JdbcUnitOfWork() { + public Void execute(Connection aConnection) throws Exception { + for (int i = tables.length - 1; i >= 0; i--) { + aOperation.execute(tables[i]); + } + return null; + } + }); + for (String table : tables) { + + } + } + + public void cleanDatabase(ITableFilterSimple aSelection) throws Exception { + + final String[] tables = getTableNames(aSelection); + executeInTransaction(new JdbcUnitOfWork() { + + public Void execute(Connection aConnection) throws Exception { + IDatabaseConnection connection = new DatabaseConnection( + aConnection); + ITableFilter filter = new DatabaseSequenceFilter(connection, + tables); + IDataSet dataset = new FilteredDataSet(filter, connection + .createDataSet(tables)); + DatabaseOperation.DELETE_ALL.execute(connection, dataset); + return null; + } + }); + + } + + public T executeInTransaction(JdbcUnitOfWork aCallback) + throws Exception { + Connection connection = dataSource.getConnection(); + try { + T value = aCallback.execute(connection); + connection.commit(); + return value; + } finally { + connection.close(); + } + } + + public String[] getTableNames() throws Exception { + return getTableNames(tables); + } + + /** + * @throws SQLException + */ + public String[] getTableNames(ITableFilterSimple aSelection) + throws Exception { + + List result = new ArrayList(); + LOG.fine("Getting database table names to clean (schema: '" + + SCHEMA_PATTERN + "'"); + + ResultSet tables = dataSource.getConnection().getMetaData().getTables( + null, SCHEMA_PATTERN, "%", new String[] { "TABLE" }); + while (tables.next()) { + String table = tables.getString("TABLE_NAME"); + if (aSelection.accept(table)) { + result.add(table); + } + } + return (String[]) result.toArray(new String[0]); + } + + public void emptyTables() throws Exception { + executeOnTables(tables, new TableSetOperation() { + public void execute(String aTable) throws Exception { + emptyTable(aTable); + } + }); + } + + /** + * @return + * @throws SQLException + */ + public void emptyTables(final ITableFilterSimple aSelection) + throws Exception { + executeOnTables(aSelection, new TableSetOperation() { + public void execute(String aTable) throws Exception { + emptyTable(aTable); + } + }); + } + + /** + * @return + * @throws SQLException + */ + public void emptyTable(String aTable) throws Exception { + executeSql("delete from " + aTable); + } + + public void dropTables() throws Exception { + executeOnTables(tables, new TableSetOperation() { + + public void execute(String aTable) throws Exception { + dropTable(aTable); + } + }); + } + + + public void dropTables(ITableFilterSimple aTables) throws Exception { + executeOnTables(aTables, new TableSetOperation() { + + public void execute(String aTable) throws Exception { + dropTable(aTable); + } + }); + } + + /** + * @return + * @throws SQLException + */ + public void dropTable(final String aTable) throws Exception { + executeInTransaction(new JdbcUnitOfWork() { + public Void execute(Connection aConnection) throws Exception { + executeUpdate(aConnection, "drop table " + aTable); + return null; + } + }); + + } + + /** + * Executes an SQL statement within a transaction. + * + * @param aSql + * SQL statement. + * @return Return code of the corresponding JDBC call. + */ + public int executeSql(final String aSql) throws Exception { + return executeSql(aSql, new Object[0]); + } + + /** + * Executes an SQL statement within a transaction. See + * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on + * supported argument types. + * + * @param aSql + * SQL statement. + * @param aArg + * Argument of the sql statement. + * @return Return code of the corresponding JDBC call. + */ + public int executeSql(final String aSql, final Object aArg) + throws Exception { + return executeSql(aSql, new Object[] { aArg }); + } + + /** + * Executes an sql statement. See + * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on + * supported argument types. + * + * @param aSql + * SQL query to execute. + * @param aArgs + * Arguments. + * @return Number of rows updated. + */ + public int executeSql(final String aSql, final Object[] aArgs) + throws Exception { + return executeInTransaction(new JdbcUnitOfWork() { + public Integer execute(Connection aConnection) throws Exception { + PreparedStatement stmt = aConnection.prepareStatement(aSql); + setPreparedParams(aArgs, stmt); + return stmt.executeUpdate(); + } + }); + } + + /** + * Executes an SQL query. + * + * @param aSql + * Query to execute. + * @return Result set. + */ + public ResultSet executeQuery(Connection aConnection, String aSql) { + return executeQuery(aConnection, aSql, new Object[0]); + } + + /** + * Executes a query with a single argument. See + * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on + * supported argument types. + * + * @param aSql + * Query. + * @param aArg + * Argument. + * @return Result set. + */ + public ResultSet executeQuery(Connection aConnection, String aSql, + Object aArg) { + return executeQuery(aConnection, aSql, new Object[] { aArg }); + } + + /** + * Executes a query. See + * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on + * supported argument types. + * + * @param aSql + * Sql query. + * @param aArgs + * Arguments to the query. + * @return Result set. + */ + public ResultSet executeQuery(Connection aConnection, final String aSql, + final Object[] aArgs) { + try { + PreparedStatement statement = aConnection.prepareStatement(aSql); + setPreparedParams(aArgs, statement); + + return statement.executeQuery(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public int executeUpdate(Connection aConnection, final String aSql, + final Object... aArgs) { + try { + PreparedStatement statement = aConnection.prepareStatement(aSql); + setPreparedParams(aArgs, statement); + + return statement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the values of a prepared statement. See + * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on + * supported argument types. + * + * @param aArgs + * Arguments to the prepared statement. + * @param aStatement + * Prepared statement + * @throws SQLException + */ + private void setPreparedParams(final Object[] aArgs, + PreparedStatement aStatement) throws SQLException { + for (int i = 1; i <= aArgs.length; i++) { + setPreparedParam(i, aStatement, aArgs[i - 1]); + } + } + + /** + * Sets a prepared statement parameter. + * + * @param aIndex + * Index of the parameter. + * @param aStatement + * Prepared statement. + * @param aObject + * Value Must be of type Integer, Long, or String. + * @throws SQLException + */ + private void setPreparedParam(int aIndex, PreparedStatement aStatement, + Object aObject) throws SQLException { + if (aObject instanceof Integer) { + aStatement.setInt(aIndex, ((Integer) aObject).intValue()); + } else if (aObject instanceof Long) { + aStatement.setLong(aIndex, ((Integer) aObject).longValue()); + } else if (aObject instanceof String) { + aStatement.setString(aIndex, (String) aObject); + } else { + TestCase.fail("Unsupported object type for prepared statement: " + + aObject.getClass() + " value: " + aObject + + " statement: " + aStatement); + } + } + + /** + * @return + * @throws SQLException + */ + public int getTableSize(final String aTable) throws Exception { + return executeInTransaction(new JdbcUnitOfWork() { + public Integer execute(Connection aConnection) throws Exception { + ResultSet resultSet = executeQuery(aConnection, + "select count(*) from " + aTable); + resultSet.next(); + return resultSet.getInt(1); + } + }); + + } + + public int countResultSet(ResultSet aResultSet) throws SQLException { + int count = 0; + + while (aResultSet.next()) { + count++; + } + + return count; + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabase.java b/support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabase.java new file mode 100755 index 00000000..646931c5 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabase.java @@ -0,0 +1,314 @@ +/* + * Copyright 2005 the original author or authors. + * + * 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 org.wamblee.support.persistence; + +import java.io.File; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import junit.framework.TestCase; + +import org.apache.derby.drda.NetworkServerControl; +import org.wamblee.io.FileSystemUtils; + +/** + * Derby database setup. The external JDBC url used to connect to a running + * instance is + * + *
+ *     jdbc:derby:net://localhost:1527/testdb
+ * 
+ * + * and the driver class is + * + *
+ * com.ibm.db2.jcc.DB2Driver
+ * 
+ * + * The following jars will have to be used db2jcc.jar and + * db2jcc_license_c.jar. + */ +public class DerbyDatabase extends AbstractDatabase { + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(DerbyDatabase.class + .getName()); + + /** + * Database user name. + */ + private static final String USERNAME = "sa"; + + /** + * Database password. + */ + private static final String PASSWORD = "123"; + /** + * Poll interval for the checking the server status. + */ + private static final int POLL_INTERVAL = 100; + + /** + * Maximum time to wait until the server has started or stopped. + */ + private static final int MAX_WAIT_TIME = 10000; + + /** + * Database name to use. + */ + private static final String DATABASE_NAME = "testdb"; + + /** + * Path on the file system where derby files are stored. + */ + private static final String DATABASE_PATH = "target/db/persistence/derby"; + + /** + * Derby property required to set the file system path + * {@link #DATABASE_PATH}. + */ + private static final String SYSTEM_PATH_PROPERTY = "derby.system.home"; + + + private boolean inmemory; + + + /** + * Constructs derby database class to allow creation of derby database + * instances. + */ + public DerbyDatabase() { + inmemory = true; + } + + public DerbyDatabase(boolean aInMemoryFlag) { + inmemory = aInMemoryFlag; + } + + /* + * (non-Javadoc) + * + * @see org.wamblee.persistence.Database#start() + */ + public void doStart() { + try { + // just in case a previous run was killed without the + // cleanup + cleanPersistentStorage(); + + if (!inmemory) { + // set database path. + Properties lProperties = System.getProperties(); + lProperties.put(SYSTEM_PATH_PROPERTY, DATABASE_PATH); + } + + Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance(); + + runDatabase(); + + waitUntilStartedOrStopped(true); + + // Force creation of the database. + Connection lConnection = createConnection(); + lConnection.close(); + + LOGGER.info("Database started: \n URL = " + getExternalJdbcUrl() + "\n user = " + getUsername() + "\n password = " + + getPassword()); + + createDataSource(); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + if (isStarted()) { + LOGGER.warning("Shutting down db"); + DerbyDatabase.this.stop(); + } + } + }); + } catch (Exception e) { + throw new RuntimeException("Problem starting database", e); + } + } + + /** + * Waits until the database server has started or stopped. + * + * @param aStarted + * If true, waits until the server is up, if false, waits until + * the server is down. + * @throws InterruptedException + */ + private void waitUntilStartedOrStopped(boolean aStarted) + throws InterruptedException { + long lWaited = 0; + + while (aStarted != isStarted()) { + Thread.sleep(POLL_INTERVAL); + lWaited += POLL_INTERVAL; + + if (lWaited > MAX_WAIT_TIME) { + throw new RuntimeException( + "Derby database did not start within " + MAX_WAIT_TIME + + "ms"); + } + } + } + + /** + * Checks if the database server has started or not. + * + * @return True if started, false otherwise. + */ + private boolean isStarted() { + try { + getControl().ping(); + + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Gets the controller for the database server. + * + * @return Controller. + * @throws Exception + */ + private NetworkServerControl getControl() throws Exception { + return new NetworkServerControl(); + } + + /** + * Runs the database. + * + */ + private void runDatabase() { + try { + getControl().start(new PrintWriter(System.out)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /* + * (non-Javadoc) + * + * @see org.wamblee.persistence.Database#getJdbcUrl() + */ + public String getJdbcUrl() { + return getBaseJdbcUrl() + + ";create=true;retrieveMessagesFromServerOnGetMessage=true;"; + } + + private String getBaseJdbcUrl() { + return (inmemory ? "jdbc:derby:memory:": "jdbc:derby:") + DATABASE_NAME; + } + + /* + * (non-Javadoc) + * + * @see org.wamblee.persistence.Database#getExternalJdbcUrl() + */ + public String getExternalJdbcUrl() { + return "jdbc:derby://localhost:1527/" + (inmemory ? "memory:" : "") + DATABASE_NAME; + } + + /** + * Shuts down the derby database and cleans up all created files. + * + */ + private void shutdownDerby() { + try { + DriverManager.getConnection("jdbc:derby:;shutdown=true"); + throw new RuntimeException("Derby did not shutdown, " + + " should always throw exception at shutdown"); + } catch (Exception e) { + LOGGER.info("Derby has been shut down."); + } + } + + /** + * Gets the user name. + */ + public String getUsername() { + return USERNAME; + } + + /** + * Gets the password. + * + * @return + */ + public String getPassword() { + return PASSWORD; + } + + /* + * (non-Javadoc) + * + * @see org.wamblee.persistence.Database#createConnection() + */ + public Connection createConnection() throws SQLException { + Connection c = DriverManager.getConnection(getJdbcUrl(), getUsername(), + getPassword()); + + return c; + } + + + /** + * Stops the derby database and cleans up all derby files. + */ + public void doStop() { + try { + // shutdown network server. + getControl().shutdown(); + waitUntilStartedOrStopped(false); + + // shutdown inmemory access. + shutdownDerby(); + cleanPersistentStorage(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Problem stopping database", e); + } + } + + /** + * Cleans up persistent storage of Derby. + */ + private void cleanPersistentStorage() { + File lFile = new File(DATABASE_PATH); + + if (lFile.isFile()) { + TestCase.fail("A regular file by the name " + DATABASE_PATH + + " exists, clean this up first"); + } + + if (!lFile.isDirectory()) { + return; // no-op already cleanup up. + } + FileSystemUtils.deleteDirRecursively(DATABASE_PATH); + } +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabaseProvider.java b/support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabaseProvider.java new file mode 100644 index 00000000..088202c3 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/DerbyDatabaseProvider.java @@ -0,0 +1,32 @@ +package org.wamblee.support.persistence; + +import java.util.Arrays; +import java.util.List; + +public class DerbyDatabaseProvider extends AbstractDatabaseProvider { + + /** + * Capabilities of this type of database. + */ + public static final List CAPABILIITIES = + Arrays.asList(DatabaseProvider.CAPABILITY_IN_MEMORY, "DERBY"); + + + public DerbyDatabaseProvider() { + // Empty + } + + public Database create() { + return new DerbyDatabase(); + } + + public DatabaseDescription getDescription() { + return new DatabaseDescription(CAPABILIITIES.toArray(new String[0]), + "Derby", "In-memory, volatile, set breakpoint to debug"); + } + + @Override + protected List getCapabilities() { + return CAPABILIITIES; + } +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabase.java b/support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabase.java new file mode 100644 index 00000000..2533df42 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabase.java @@ -0,0 +1,82 @@ +package org.wamblee.support.persistence; + +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp.ConnectionFactory; +import org.apache.commons.dbcp.DriverManagerConnectionFactory; +import org.apache.commons.dbcp.PoolableConnectionFactory; +import org.apache.commons.dbcp.PoolingDataSource; +import org.apache.commons.pool.impl.GenericObjectPool; + +/** + * Database that encapsulates connection to an external database. + * Database connection details can be configured through system properties + * and environment variables, see {@link #DB_URL_PROP}, {@link #DB_USER_PROP}, and + * {@link #DB_PASSWORD_PROP|. + * + * This class assumes modern database drivers that work together with java.util.ServiceLoader + * so that explicitly doing a Class.forName() is not necessary to load the database driver. + */ +public class ExternalDatabase extends AbstractDatabase { + + private static final Logger LOGGER = Logger.getLogger(ExternalDatabase.class.getName()); + + /** + * System property/environment variable that defines the database URL. + */ + public static final String DB_URL_PROP = "TEST_DB_URL"; + + /** + * System property/environment variable that defines the database user. + */ + public static final String DB_USER_PROP = "TEST_DB_USER"; + + /** + * System property/environment variable that defines the database password. + */ + public static final String DB_PASSWORD_PROP = "TEST_DB_PASSWORD"; + + + private String itsUrl; + private String itsUser; + private String itsPassword; + + private DataSource itsDataSource; + + public ExternalDatabase() { + // Empty + } + + public String getExternalJdbcUrl() { + return itsUrl; + } + + public String getJdbcUrl() { + return itsUrl; + } + + public String getPassword() { + return itsPassword; + } + + public String getUsername() { + return itsUser; + } + + public void doStart() { + itsUrl = getProperty(DB_URL_PROP); + itsUser = getProperty(DB_USER_PROP); + itsPassword = getProperty(DB_PASSWORD_PROP); + + createDataSource(); + } + + public void doStop() { + // Empty. + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabaseProvider.java b/support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabaseProvider.java new file mode 100644 index 00000000..871be11a --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/ExternalDatabaseProvider.java @@ -0,0 +1,35 @@ +package org.wamblee.support.persistence; + +import java.util.Arrays; +import java.util.List; + +public class ExternalDatabaseProvider extends AbstractDatabaseProvider { + + /** + * Capabilities of this type of database. + */ + public static final List CAPABILIITIES = Arrays + .asList(CAPABILITY_EXTERNAL); + + @Override + protected List getCapabilities() { + return CAPABILIITIES; + } + + public Database create() { + return new ExternalDatabase(); + } + + public DatabaseDescription getDescription() { + return new DatabaseDescription( + CAPABILIITIES.toArray(new String[0]), + "External Database", + "Any database as described by the JDBC URL: requires system properties or environment variables: " + + ExternalDatabase.DB_URL_PROP + + ", " + + ExternalDatabase.DB_USER_PROP + + ", and " + + ExternalDatabase.DB_PASSWORD_PROP); + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/JpaBuilder.java b/support/test/src/main/java/org/wamblee/support/persistence/JpaBuilder.java new file mode 100644 index 00000000..b46ffa3a --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/JpaBuilder.java @@ -0,0 +1,134 @@ +package org.wamblee.support.persistence; + +import java.sql.SQLException; +import java.util.Map; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; +import javax.persistence.Persistence; +import javax.sql.DataSource; + +import org.wamblee.support.jndi.StubInitialContextFactory; +import org.wamblee.support.persistence.toplink.JndiSessionCustomizer; + + +/** + * Utility for building an appropriately configured EntityManagerFactory. The + * idea is that a persistence.xml is used unchanged from the production version. + * This utility will then add the additional properties required for execution + * in a standalone environment. + * + * The other purpose is to to shield dependencies of the test code on a + * particular JPA provider. + */ +public class JpaBuilder { + + private static final Logger LOGGER = Logger.getLogger(JpaBuilder.class + .getName()); + + /** + * Callback interface to execute some JPA code within a transaction with the + * entitymanager to use provided as input. + */ + public static interface JpaUnitOfWork { + /** + * Executes the unit of work. A transaction has been started. + * @param em Entity manager. + * @return Result of the execute method. If you don't want to return anything use + * Void for the return type and return null from the implementation. + */ + T execute(EntityManager em); + } + + private PersistenceUnitDescription persistenceUnit; + private DataSource dataSource; + private EntityManagerFactory factory; + + /** + * Constructs the builder. + * + * @param aDataSource + * Datasource of database. + * @param aPersistenceUnit + * Persistence unit. + */ + public JpaBuilder(DataSource aDataSource, + PersistenceUnitDescription aPersistenceUnit) { + persistenceUnit = aPersistenceUnit; + dataSource = aDataSource; + StubInitialContextFactory.register(); + } + + /** + * Starts the builder, which in particular, mocks JNDI, binds the datasource + * the JNDI where the persistence unit expects it, creates the entity + * manager factory, and forces creation of the database schema. + */ + public void start() throws Exception { + try { + InitialContext ctx = new InitialContext(); + ctx.bind(persistenceUnit.getJndiName(), dataSource); + } catch (NamingException e) { + throw new RuntimeException("JNDI problem", e); + } + factory = createFactory(); + execute(new JpaUnitOfWork() { + public Void execute(EntityManager em) { + // Empty, just to trigger database schema creation. + return null; + } + }); + } + + /** + * Stops the entity manager factory and disables JNDI mocking. + */ + public void stop() { + StubInitialContextFactory.unregister(); + factory.close(); + } + + /** + * Creates a new entity manager factory. Typically not used by test code. + * @return Entity manager factory. + */ + public EntityManagerFactory createFactory() { + Map jpaProps = new TreeMap(); + jpaProps.put("toplink.session.customizer", JndiSessionCustomizer.class + .getName()); + jpaProps.put("toplink.ddl-generation", "create-tables"); + return Persistence.createEntityManagerFactory(persistenceUnit + .getUnitName(), jpaProps); + } + + /** + * Executes a unit of work. This creates an entitymanager and runs the + * {@link JpaUnitOfWork#execute(EntityManager)} within a transaction, passing + * it the entity manager. Use of this method saves a lot of typing for applications. + * + * @param aWork Work to execute. + * @return The return value of the execute method of the unit of work. + */ + public T execute(JpaUnitOfWork aWork) throws Exception { + EntityManager em = factory.createEntityManager(); + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + try { + T value = aWork.execute(em); + transaction.commit(); + return value; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Exception occured", e); + transaction.rollback(); + throw e; + } finally { + em.close(); + } + } +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/JpaTester.java b/support/test/src/main/java/org/wamblee/support/persistence/JpaTester.java new file mode 100644 index 00000000..c0a22cd3 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/JpaTester.java @@ -0,0 +1,113 @@ +package org.wamblee.support.persistence; + +import javax.sql.DataSource; + +import org.dbunit.IDatabaseTester; + +/** + * This class is the entry point for JPA tests. Test code should construct a JpaTester in the + * @Before method and call {@link #start()} on it in that method. Also, test code should + * call {@link #stop()} on it in the @After method. + * + * This class is constructed with a description of the persistence unit to be tested. The principle is that + * an existing persistence.xml can be tested without change in unit test code. + * + * It then takes care of the following: + *
    + *
  • Creating an inmemory database for testing (default) or connecting to an external database. + * See {@link DatabaseBuilder} for more information on how a databse is obtained. + *
  • + *
  • Drop all database tables that are related to the persistence unit under test, including JPA provider + * specific tables. + *
  • + *
  • Creating a datasource for the database and make the datasource available through JNDI. + *
  • + *
  • Creating the entity manager factory for JPA and configuring it in such a way that schema creation + * happens. (Typically, schema creation will be disabled in the persistence.xml but this utility enables it + * for unit test). + *
  • + *
  • Creating a DBUnit database tester which is appropriately configured for the persistence unit under test. + *
  • + *
+ * + * The main entry point for all this functionality is the {@link PersistenceUnitDescription} which describes the + * persistence unit and must be provided at construction of the JpaTester + * + * NOTE: Persistence XML files should be explicitly configured with the classes that are part of the persistence unit + * since scanning of classes does not work correctly in a unit test environment. This is currently the only limitation. + */ +public class JpaTester { + + private PersistenceUnitDescription persistenceUnit; + private Database db; + private DataSource dataSource; + private DatabaseUtils dbUtils; + private JpaBuilder jpaBuilder; + private IDatabaseTester dbTester; + + /** + * Constructs the tester. + * @param aPersistenceUnit Persistence unit under test. + */ + public JpaTester(PersistenceUnitDescription aPersistenceUnit) { + persistenceUnit = aPersistenceUnit; + } + + /** + * Starts the tester. This must be called prior to running the test. + * @throws Exception + */ + public void start() throws Exception { + db = DatabaseBuilder.getDatabase(); + dataSource = db.start(); + + dbUtils = new DatabaseUtils(dataSource, persistenceUnit.getTables()); + dbUtils.dropTables(); + dbUtils.dropTables(persistenceUnit.getJpaTables()); + + jpaBuilder = new JpaBuilder(dataSource, persistenceUnit); + jpaBuilder.start(); + + // db tester should be created after Jpa builder because jpa builder + // creates the + // tables that the tester looks at when it is initialized. + dbTester = dbUtils.createDbTester(); + } + + /** + * Stops the tester. This must be called after the test. + */ + public void stop() { + if (jpaBuilder != null) { + jpaBuilder.stop(); + } + if (db != null) { + db.stop(); + } + } + + public Database getDb() { + return db; + } + + public DataSource getDataSource() { + return dataSource; + } + + public IDatabaseTester getDbTester() { + return dbTester; + } + + public DatabaseUtils getDbUtils() { + return dbUtils; + } + + public JpaBuilder getJpaBuilder() { + return jpaBuilder; + } + + public PersistenceUnitDescription getPersistenceUnit() { + return persistenceUnit; + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/PersistenceUnitDescription.java b/support/test/src/main/java/org/wamblee/support/persistence/PersistenceUnitDescription.java new file mode 100644 index 00000000..502f43db --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/PersistenceUnitDescription.java @@ -0,0 +1,40 @@ +package org.wamblee.support.persistence; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.dbunit.dataset.filter.ITableFilterSimple; + +public class PersistenceUnitDescription { + + private String jndiName; + private String unitName; + private ITableFilterSimple tables; + + public PersistenceUnitDescription(String aJndiName, String aUnitName, ITableFilterSimple aTables) { + jndiName = aJndiName; + unitName = aUnitName; + tables = aTables; + } + + public String getJndiName() { + return jndiName; + } + + public String getUnitName() { + return unitName; + } + + public ITableFilterSimple getTables() { + return tables; + } + + /** + * JPA provider specific tables. + * @return + */ + public ITableFilterSimple getJpaTables() { + return new ToplinkTables(); + } +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/ToplinkTables.java b/support/test/src/main/java/org/wamblee/support/persistence/ToplinkTables.java new file mode 100644 index 00000000..4d45d0f4 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/ToplinkTables.java @@ -0,0 +1,20 @@ +package org.wamblee.support.persistence; + +import java.util.Arrays; +import java.util.List; + +import org.dbunit.dataset.DataSetException; +import org.dbunit.dataset.filter.ITableFilterSimple; + +/** + * Toplink-specific tables. + */ +public class ToplinkTables implements ITableFilterSimple { + + private static final List TABLES = Arrays.asList(new String[] { "SEQUENCE" } ); + + public boolean accept(String aTableName) throws DataSetException { + return TABLES.contains(aTableName); + } + +} diff --git a/support/test/src/main/java/org/wamblee/support/persistence/package-info.java b/support/test/src/main/java/org/wamblee/support/persistence/package-info.java new file mode 100644 index 00000000..1bd7050c --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/package-info.java @@ -0,0 +1,23 @@ +/** + * This package provide a number of utilities for database testing and in particular with + * JPA. + * + * The following utilities are available: + *
    + *
  • {@link JpaTester}: The main entry point for all JPA tests. + *
  • + *
  • {@link JpaBuilder}: A utility constructed by JpaTester that provides a callback based + * style of working with transaction-scoped entity managers. + *
  • + *
  • {@link DatabaseUtils}: A utility constructed by JpaTester for working with databases in general. Test code will not use this + * utility often. + *
  • + *
  • {@link org.dbunit.IDatabaseTester}: A DB unit database tester. The test code can use this database tester. + * It is also created by JpaTester + *
  • + *
  • {@link DatabaseBuilder}: A utility by which test code can transparently create an inmemory database or + * connect to an external database. This is also used by JpaTester + *
  • + *
+ */ +package org.wamblee.support.persistence; diff --git a/support/test/src/main/java/org/wamblee/support/persistence/toplink/JndiSessionCustomizer.java b/support/test/src/main/java/org/wamblee/support/persistence/toplink/JndiSessionCustomizer.java new file mode 100644 index 00000000..814fe593 --- /dev/null +++ b/support/test/src/main/java/org/wamblee/support/persistence/toplink/JndiSessionCustomizer.java @@ -0,0 +1,59 @@ +package org.wamblee.support.persistence.toplink; + +import javax.naming.Context; +import javax.naming.InitialContext; + +import oracle.toplink.essentials.jndi.JNDIConnector; +import oracle.toplink.essentials.sessions.DatabaseLogin; +import oracle.toplink.essentials.sessions.Session; +import oracle.toplink.essentials.threetier.ServerSession; +import oracle.toplink.essentials.tools.sessionconfiguration.SessionCustomizer; + +/** + * See http://wiki.eclipse.org/Customizing_the_EclipseLink_Application_(ELUG) Use for clients that would like to use a + * JTA SE pu instead of a RESOURCE_LOCAL SE pu. + * + * This utility also makes sure that using a persistence.xml with a JTA datasource works in a standalone Java SE + * environment together with our JNDI stub. + */ +public class JndiSessionCustomizer + implements SessionCustomizer { + + public JndiSessionCustomizer() { + // Empty. + } + + /** + * Get a dataSource connection and set it on the session with lookupType=STRING_LOOKUP + */ + public void customize(Session session) throws Exception { + JNDIConnector connector = null; + Context context = null; + try { + context = new InitialContext(); + if(null != context) { + connector = (JNDIConnector)session.getLogin().getConnector(); // possible CCE + // Change from COMPOSITE_NAME_LOOKUP to STRING_LOOKUP + // Note: if both jta and non-jta elements exist this will only change the first one - and may still result in + // the COMPOSITE_NAME_LOOKUP being set + // Make sure only jta-data-source is in persistence.xml with no non-jta-data-source property set + connector.setLookupType(JNDIConnector.STRING_LOOKUP); + + // Or, if you are specifying both JTA and non-JTA in your persistence.xml then set both connectors to be safe + JNDIConnector writeConnector = (JNDIConnector)session.getLogin().getConnector(); + writeConnector.setLookupType(JNDIConnector.STRING_LOOKUP); + JNDIConnector readConnector = + (JNDIConnector)((DatabaseLogin)((ServerSession)session).getReadConnectionPool().getLogin()).getConnector(); + readConnector.setLookupType(JNDIConnector.STRING_LOOKUP); + + System.out.println("JndiSessionCustomizer: configured " + connector.getName()); + } + else { + throw new Exception("JndiSessionCustomizer: Context is null"); + } + } + catch(Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/support/test/src/main/resources/META-INF/services/org.wamblee.support.persistence.DatabaseProvider b/support/test/src/main/resources/META-INF/services/org.wamblee.support.persistence.DatabaseProvider new file mode 100644 index 00000000..62979fb3 --- /dev/null +++ b/support/test/src/main/resources/META-INF/services/org.wamblee.support.persistence.DatabaseProvider @@ -0,0 +1,2 @@ +org.wamblee.support.persistence.DerbyDatabaseProvider +org.wamblee.support.persistence.ExternalDatabaseProvider diff --git a/support/test/src/test/java/org/wamblee/support/jndi/StubInitiaContextFactoryTest.java b/support/test/src/test/java/org/wamblee/support/jndi/StubInitiaContextFactoryTest.java new file mode 100644 index 00000000..6016e2b3 --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/jndi/StubInitiaContextFactoryTest.java @@ -0,0 +1,41 @@ +package org.wamblee.support.jndi; + +import static junit.framework.Assert.assertEquals; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.wamblee.support.jndi.StubInitialContextFactory; + +public class StubInitiaContextFactoryTest { + + @Before + @After + public void setUp() { + StubInitialContextFactory.unregister(); + } + + + @Test(expected = NamingException.class) + public void testLookupNotRegistered() throws Exception { + InitialContext ctx = new InitialContext(); + ctx.bind("a/b", "hallo"); + } + + @Test + public void testLookup() throws Exception { + StubInitialContextFactory.register(); + + InitialContext ctx = new InitialContext(); + ctx.bind("a/b", "hallo"); + + ctx = new InitialContext(); + Object obj = ctx.lookup("a/b"); + + assertEquals("hallo", obj); + } + +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/DatabaseBuilderTest.java b/support/test/src/test/java/org/wamblee/support/persistence/DatabaseBuilderTest.java new file mode 100644 index 00000000..51364876 --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/DatabaseBuilderTest.java @@ -0,0 +1,16 @@ +package org.wamblee.support.persistence; + +import org.junit.Test; +import org.wamblee.support.persistence.DatabaseBuilder; +import org.wamblee.support.persistence.DatabaseDescription; + +public class DatabaseBuilderTest { + + + @Test + public void testListAvailableDatabases() { + for (DatabaseDescription description: DatabaseBuilder.getSupportedDatabases()) { + System.out.println(description); + } + } +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/DatabaseUtilsTest.java b/support/test/src/test/java/org/wamblee/support/persistence/DatabaseUtilsTest.java new file mode 100644 index 00000000..e57f01a9 --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/DatabaseUtilsTest.java @@ -0,0 +1,89 @@ +package org.wamblee.support.persistence; + +import static junit.framework.Assert.assertEquals; + +import javax.persistence.EntityManager; +import javax.sql.DataSource; + +import org.dbunit.IDatabaseTester; +import org.dbunit.dataset.ITable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.wamblee.support.persistence.Database; +import org.wamblee.support.persistence.DatabaseBuilder; +import org.wamblee.support.persistence.DatabaseUtils; +import org.wamblee.support.persistence.JpaBuilder; +import org.wamblee.support.persistence.PersistenceUnitDescription; +import org.wamblee.support.persistence.JpaBuilder.JpaUnitOfWork; + +public class DatabaseUtilsTest { + private Database db; + private DataSource dataSource; + private PersistenceUnitDescription persistenceUnit; + private JpaBuilder builder; + private DatabaseUtils dbutils; + private IDatabaseTester dbtester; + + @Before + public void setUp() throws Exception { + db = DatabaseBuilder.getDatabase(); + dataSource = db.start(); + + persistenceUnit = new MyPersistenceUnit(); + + dbutils = new DatabaseUtils(dataSource, persistenceUnit.getTables()); + dbutils.dropTables(); + dbutils.dropTables(persistenceUnit.getJpaTables()); + + builder = new JpaBuilder(dataSource, persistenceUnit); + builder.start(); + + dbtester = dbutils.createDbTester(); + } + + @After + public void tearDown() { + builder.stop(); + db.stop(); + } + + @Test + public void testTablesCorrect() throws Exception { + String[] tables = dbutils.getTableNames(); + assertEquals(1, tables.length); + assertEquals("XYZ_MYENTITY", tables[0]); + } + + @Test + public void testDeleteTables() throws Exception { + String[] tables = dbutils.getTableNames(); + assertEquals(1, tables.length); + assertEquals("XYZ_MYENTITY", tables[0]); + + // Put some data in the database. + builder.execute(new JpaUnitOfWork() { + public Void execute(EntityManager em) { + MyEntity entity = new MyEntity("a", "b"); + em.persist(entity); + return null; + } + }); + + // Verify one row is written (using Db unit) + ITable table = dbtester.getDataSet().getTable("XYZ_MYENTITY"); + assertEquals(1, table.getRowCount()); + + // Clean the database + dbutils.cleanDatabase(); + table = dbtester.getDataSet().getTable("XYZ_MYENTITY"); + assertEquals(0, table.getRowCount()); + + // Now drop the database + dbutils.dropTables(); + dbutils.dropTables(persistenceUnit.getJpaTables()); + tables = dbutils.getTableNames(); + assertEquals(0, tables.length); + } + +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/DerbyDatabaseTest.java b/support/test/src/test/java/org/wamblee/support/persistence/DerbyDatabaseTest.java new file mode 100644 index 00000000..ffc17aea --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/DerbyDatabaseTest.java @@ -0,0 +1,47 @@ +package org.wamblee.support.persistence; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.wamblee.support.persistence.Database; +import org.wamblee.support.persistence.DatabaseBuilder; + + +public class DerbyDatabaseTest { + + private Database db; + private DataSource ds; + + @Before + public void setUp() { + db = DatabaseBuilder.getDatabase(); + ds = db.start(); + } + + @After + public void tearDown() { + db.stop(); + } + + @Test + public void testConnect() throws Exception { + Connection conn = ds.getConnection(); + try { + System.out.println("Database name: " + conn.getMetaData().getDatabaseProductName()); + } finally { + conn.close(); + } + } + + @Test + public void testUseASecondTimeInTheSameTestCase() throws Exception { + testConnect(); + tearDown(); + setUp(); + testConnect(); + } +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/ExternalDatabaseTest.java b/support/test/src/test/java/org/wamblee/support/persistence/ExternalDatabaseTest.java new file mode 100644 index 00000000..dbe5800b --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/ExternalDatabaseTest.java @@ -0,0 +1,53 @@ +package org.wamblee.support.persistence; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.wamblee.support.persistence.Database; +import org.wamblee.support.persistence.DatabaseBuilder; +import org.wamblee.support.persistence.DatabaseProvider; +import org.wamblee.support.persistence.ExternalDatabase; + +import static junit.framework.TestCase.*; + +public class ExternalDatabaseTest { + + @Test + public void testExternalDB() throws Exception { + // Connect to inmemory db using External database class. + + Database inmemory = DatabaseBuilder + .getDatabase(DatabaseProvider.CAPABILITY_IN_MEMORY); + try { + inmemory.start(); + + System.setProperty(ExternalDatabase.DB_URL_PROP, inmemory + .getExternalJdbcUrl()); + System.setProperty(ExternalDatabase.DB_USER_PROP, inmemory + .getUsername()); + System.setProperty(ExternalDatabase.DB_PASSWORD_PROP, inmemory + .getPassword()); + + Database external = DatabaseBuilder + .getDatabase(DatabaseProvider.CAPABILITY_EXTERNAL); + assertTrue(external instanceof ExternalDatabase); + try { + DataSource ds = external.start(); + Connection conn = ds.getConnection(); + try { + System.out.println("Database name: " + + conn.getMetaData().getDatabaseProductName()); + } finally { + conn.close(); + } + } finally { + external.stop(); + } + } finally { + inmemory.stop(); + } + + } +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/MyEntity.java b/support/test/src/test/java/org/wamblee/support/persistence/MyEntity.java new file mode 100644 index 00000000..c2b47288 --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/MyEntity.java @@ -0,0 +1,39 @@ +package org.wamblee.support.persistence; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "XYZ_MYENTITY") +public class MyEntity { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String sleuteltje; + private String value; + + + public MyEntity() { + // Empty + } + + public MyEntity(String aKey, String aValue) { + sleuteltje = aKey; + value = aValue; + } + + public String getKey() { + return sleuteltje; + } + + public String getValue() { + return value; + } + + +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/MyEntityExampleTest.java b/support/test/src/test/java/org/wamblee/support/persistence/MyEntityExampleTest.java new file mode 100644 index 00000000..e7b86fc9 --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/MyEntityExampleTest.java @@ -0,0 +1,87 @@ +package org.wamblee.support.persistence; + +import javax.persistence.EntityManager; +import javax.persistence.Persistence; +import javax.sql.DataSource; + +import org.dbunit.DataSourceDatabaseTester; +import org.dbunit.DatabaseTestCase; +import org.dbunit.IDatabaseTester; +import org.dbunit.dataset.ITable; +import org.dbunit.dataset.filter.ITableFilterSimple; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.wamblee.support.persistence.DatabaseUtils; +import org.wamblee.support.persistence.JpaBuilder; +import org.wamblee.support.persistence.JpaTester; +import org.wamblee.support.persistence.JpaBuilder.JpaUnitOfWork; + +import static junit.framework.Assert.*; + + +/** + * This class shows an example of how to test an entity using jpa. + */ +public class MyEntityExampleTest { + + // This is the magical object that does all the test setup. + private JpaTester jpaTester; + + // The jpa tester initializes a lot for us.... + + // A JPA builder that provides a transaction scoped entity manager for us. + private JpaBuilder builder; + + // The database tester for dbunit which is appropriately configured for our persistence unit. + private IDatabaseTester dbtester; + + // Database utilities with some additional functionality for working with the databse + // such as dropping tables, cleaning tables, etc. + private DatabaseUtils dbutils; + + @Before + public void setUp() throws Exception { + + // First we create the JpaTester by telling us which persistence unit we are going to test + jpaTester = new JpaTester(new MyPersistenceUnit()); + jpaTester.start(); + + // Retrieve some useful objects fromt he jpa tester. It also provides direct access to the datasource + // but we don't need it. We can use datbase utils if we want to execute straight JDBC calls. + builder = jpaTester.getJpaBuilder(); + dbtester = jpaTester.getDbTester(); + dbutils = jpaTester.getDbUtils(); + } + + @After + public void tearDown() { + jpaTester.stop(); + } + + @Test + public void testEntityPersistence() throws Exception { + + // Use the JPA builder to create a transaction scoped entity manager for as and execute the + // unit of work. + builder.execute(new JpaUnitOfWork() { + public Void execute(EntityManager em) { + MyEntity entity = new MyEntity("a", "b"); + em.persist(entity); + return null; + } + }); + + // Verify one row is written (using Db unit) + ITable table = dbtester.getDataSet().getTable("XYZ_MYENTITY"); + assertEquals(1, table.getRowCount()); + + assertEquals("a", table.getValue(0, "SLEUTELTJE")); + assertEquals("b", table.getValue(0, "VALUE")); + + // For this simple test, it can also be done through DatabaseUtils + assertEquals(1, dbutils.getTableSize("XYZ_MYENTITY")); + + } + +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/MyPersistenceUnit.java b/support/test/src/test/java/org/wamblee/support/persistence/MyPersistenceUnit.java new file mode 100644 index 00000000..777dd882 --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/MyPersistenceUnit.java @@ -0,0 +1,15 @@ +package org.wamblee.support.persistence; + +import org.wamblee.support.persistence.PersistenceUnitDescription; + +/** + * This class describes the persistence unit that we are testing. + */ +public class MyPersistenceUnit extends PersistenceUnitDescription { + private static final String JNDI_NAME = "wamblee/support/test"; + private static final String PERSISTENCE_UNIT_NAME = "org.wamblee.jee.support-test"; + + public MyPersistenceUnit() { + super(JNDI_NAME, PERSISTENCE_UNIT_NAME, new MyTables()); + } +} diff --git a/support/test/src/test/java/org/wamblee/support/persistence/MyTables.java b/support/test/src/test/java/org/wamblee/support/persistence/MyTables.java new file mode 100644 index 00000000..1283a8ff --- /dev/null +++ b/support/test/src/test/java/org/wamblee/support/persistence/MyTables.java @@ -0,0 +1,25 @@ +package org.wamblee.support.persistence; + +import org.dbunit.dataset.DataSetException; +import org.dbunit.dataset.filter.ITableFilterSimple; + +/** + * This class describes the tables we are testing. + * This implementation selects all tables with the "XYZ" prefix. + */ +public class MyTables implements ITableFilterSimple { + + /** + * + * @param aJpaTables Specific tables used by the JPA provider in addition to those for the + * entities. + */ + public MyTables() { + // Empty. + } + + public boolean accept(String tableName) throws DataSetException { + return tableName.startsWith("XYZ_"); + } + +} diff --git a/support/test/src/test/resources/META-INF/persistence.xml b/support/test/src/test/resources/META-INF/persistence.xml new file mode 100644 index 00000000..c2528813 --- /dev/null +++ b/support/test/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,40 @@ + + + + wamblee/support/test + org.wamblee.support.persistence.MyEntity + + + + + + + + + + + + + + + + + -- 2.31.1