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

org.unitils.database.DatabaseModule Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2008, Unitils.org
 * 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.unitils.database;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.unitils.core.Module;
import org.unitils.core.TestListener;
import org.unitils.core.Unitils;
import org.unitils.core.UnitilsException;
import org.unitils.core.config.Configuration;
import org.unitils.core.dbsupport.DefaultSQLHandler;
import org.unitils.core.dbsupport.SQLHandler;
import org.unitils.database.annotations.TestDataSource;
import org.unitils.database.annotations.Transactional;
import org.unitils.database.config.DataSourceFactory;
import org.unitils.database.config.DatabaseConfiguration;
import org.unitils.database.config.DatabaseConfigurations;
import org.unitils.database.config.DatabaseConfigurationsFactory;
import org.unitils.database.transaction.UnitilsTransactionManager;
import org.unitils.database.transaction.impl.UnitilsTransactionManagementConfiguration;
import org.unitils.database.util.Flushable;
import org.unitils.database.util.TransactionMode;
import org.unitils.dbmaintainer.DBMaintainer;
import org.unitils.util.PropertyUtils;
import org.unitils.util.ReflectionUtils;

import static org.unitils.core.util.ConfigUtils.getInstanceOf;
import static org.unitils.database.util.TransactionMode.COMMIT;
import static org.unitils.database.util.TransactionMode.DEFAULT;
import static org.unitils.database.util.TransactionMode.DISABLED;
import static org.unitils.database.util.TransactionMode.ROLLBACK;
import static org.unitils.util.AnnotationUtils.getFieldsAnnotatedWith;
import static org.unitils.util.AnnotationUtils.getMethodOrClassLevelAnnotationProperty;
import static org.unitils.util.AnnotationUtils.getMethodsAnnotatedWith;
import static org.unitils.util.ModuleUtils.getAnnotationPropertyDefaults;
import static org.unitils.util.ModuleUtils.getEnumValueReplaceDefault;

/**
 * Module that provides support for database testing: Creation of a datasource
 * that connects to the test database, support for executing tests in a
 * transaction and automatic maintenance of the test database.
 * 

* A datasource will be created the first time one is requested. Which type of * datasource will be created depends on the configured * {@link DataSourceFactory}. By default this will be a pooled datasource that * gets its connection-url, username and password from the unitils * configuration. *

* The created datasource can be injected into a field of the test by annotating * the field with {@link TestDataSource}. It can then be used to install it in * your DAO or other class under test. *

* If the DBMaintainer is enabled (by setting * {@link #PROPERTY_UPDATEDATABASESCHEMA_ENABLED} to true), the test database * schema will automatically be updated if needed. This check will be performed * once during your test-suite run, namely when the data source is created. *

* If the test class or method is annotated with {@link Transactional} with * transaction mode {@link TransactionMode#COMMIT} or * {@link TransactionMode#ROLLBACK}, or if the property * 'DatabaseModule.Transactional.value.default' was set to 'commit' or * 'rollback', every test is executed in a transaction. * * @author Filip Neven * @author Tim Ducheyne * @see TestDataSource * @see DBMaintainer * @see Transactional */ public class DatabaseModule implements Module { /** * Property indicating if the database schema should be updated before * performing the tests */ public static final String PROPERTY_UPDATEDATABASESCHEMA_ENABLED = "updateDataBaseSchema.enabled"; /** * Sets all the sequences to the lowest acceptable value. * This can be defined with the property "sequenceUpdater.sequencevalue.lowestacceptable". */ public static final String PROPERTY_RESET_SEQUENCES = "reset.sequences.to.lowest.value.before.test"; /** * Property indicating whether the datasource injected onto test fields * annotated with @TestDataSource or retrieved using * {@link DataSourceWrapper#getTransactionalDataSourceAndActivateTransactionIfNeeded(Object)} * must be wrapped in a transactional proxy */ public static final String PROPERTY_WRAP_DATASOURCE_IN_TRANSACTIONAL_PROXY = "dataSource.wrapInTransactionalProxy"; /* The logger instance for this class */ private static Logger logger = LoggerFactory.getLogger(DatabaseModule.class); /** * Map holding the default configuration of the database module annotations */ protected Map, Map> defaultAnnotationPropertyValues; /** * The datasources with the name as key */ protected DataSource dataSource; /** * The configuration of Unitils */ protected Properties configuration; /** * Indicates if the DBMaintainer should be invoked to update the database */ protected boolean updateDatabaseSchemaEnabled; /** * Indicates whether the datasource injected onto test fields annotated with * * @TestDataSource or retrieved using * {@link DataSourceWrapper#getTransactionalDataSourceAndActivateTransactionIfNeeded} must be * wrapped in a transactional proxy */ protected boolean wrapDataSourceInTransactionalProxy; /** * The transaction manager */ protected UnitilsTransactionManager transactionManager; /** * Set of possible providers of a spring * PlatformTransactionManager */ protected Set transactionManagementConfigurations = new HashSet<>(); // protected String dialect; private DatabaseConfigurations databaseConfigurations; // protected DataSourceWrapper wrapper; protected Map wrappers = new HashMap<>(); /** * Initializes this module using the given Configuration * * @param configuration * The config, not null */ @Override public void init(Properties configuration) { this.configuration = configuration; DatabaseConfigurationsFactory configFactory = new DatabaseConfigurationsFactory(new Configuration(configuration)); databaseConfigurations = configFactory.create(); defaultAnnotationPropertyValues = getAnnotationPropertyDefaults(DatabaseModule.class, configuration, Transactional.class); updateDatabaseSchemaEnabled = PropertyUtils.getBoolean(PROPERTY_UPDATEDATABASESCHEMA_ENABLED, configuration); wrapDataSourceInTransactionalProxy = PropertyUtils.getBoolean(PROPERTY_WRAP_DATASOURCE_IN_TRANSACTIONAL_PROXY, configuration); PlatformTransactionManager.class.getName(); } /** * Initializes the spring support object */ @Override public void afterInit() { // do nothing } public void registerTransactionManagementConfiguration() { for (DataSourceWrapper wrapper : wrappers.values()) { registerTransactionManagementConfiguration(wrapper); } } public void registerTransactionManagementConfiguration(final DataSourceWrapper wrapper) { // Make sure that a spring DataSourceTransactionManager is used for transaction management, if // no other transaction management configuration takes preference registerTransactionManagementConfiguration(new UnitilsTransactionManagementConfiguration() { @Override public boolean isApplicableFor(Object testObject) { return true; } @Override public PlatformTransactionManager getSpringPlatformTransactionManager(Object testObject) { return new DataSourceTransactionManager(wrapper.getDataSourceAndActivateTransactionIfNeeded()); } @Override public boolean isTransactionalResourceAvailable(Object testObject) { return wrapper.isDataSourceLoaded(); } @Override public Integer getPreference() { return 1; } }); } public void activateTransactionIfNeeded() { if (transactionManager != null) { transactionManager.activateTransactionIfNeeded(getTestObject()); } } /** * Returns the transaction manager or creates one if it does not exist yet. * * @return The transaction manager, not null */ public UnitilsTransactionManager getTransactionManager() { if (transactionManager == null) { transactionManager = getInstanceOf(UnitilsTransactionManager.class, configuration); } transactionManager.init(transactionManagementConfigurations); return transactionManager; } /** * Flushes all pending updates to the database. This method is useful when * the effect of updates needs to be checked directly on the database. *

* Will look for modules that implement {@link Flushable} and call * {@link Flushable#flushDatabaseUpdates(Object)} on these modules. * * @param testObject * The test object, not null */ public void flushDatabaseUpdates(Object testObject) { List flushables = Unitils.getInstance().getModulesRepository().getModulesOfType(Flushable.class); for (Flushable flushable : flushables) { flushable.flushDatabaseUpdates(testObject); } } /** * Updates the database version to the current version, without issuing any * other updates to the database. This method can be used for example after * you've manually brought the database to the latest version, but the * database version is not yet set to the current one. This method can also * be useful for example for reinitializing the database after having * reorganized the scripts folder. * * @param sqlHandler * The {@link DefaultSQLHandler} to which all commands are * issued */ public void resetDatabaseState(SQLHandler sqlHandler, DataSourceWrapper wrapper) { String schema = wrapper.databaseConfiguration.getDefaultSchemaName(); if (!StringUtils.isEmpty(schema)) { DatabaseConfiguration databaseConfiguration = wrapper.getDatabaseConfiguration(); DBMaintainer dbMaintainer = new DBMaintainer(configuration, sqlHandler, databaseConfiguration.getDialect(), databaseConfiguration.getSchemaNames()); dbMaintainer.resetDatabaseState(schema, wrapper.getDatabaseConfiguration().isDefaultDatabase()); } else { logger.debug("No schema found! The database is not reset!"); } } /** * Assigns the TestDataSource to every field annotated with * {@link TestDataSource} and calls all methods annotated with * {@link TestDataSource} * * @param testObject * The test instance, not null */ public void injectDataSource(Object testObject) { Set fields = getFieldsAnnotatedWith(testObject.getClass(), TestDataSource.class); Set methods = getMethodsAnnotatedWith(testObject.getClass(), TestDataSource.class); Map mapDatasources = new HashMap<>(); // update all databases for (Entry wrapper : wrappers.entrySet()) { DataSource dataSource2 = getDataSource(wrapper.getKey(), mapDatasources, testObject); // look if datasource is needed in test. setFieldDataSource(wrapper.getKey(), dataSource2, testObject, fields, methods); } } protected void setFieldDataSource(String databaseName, DataSource dataSource, Object testObject, Set fields, Set methods) { if (fields.isEmpty() && methods.isEmpty()) { // Nothing to do. Jump out to make sure that we don't try to instantiate the DataSource return; } for (Field field : fields) { TestDataSource annotation = field.getAnnotation(TestDataSource.class); if (annotation == null) { continue; } String tempDatabaseName = StringUtils.isEmpty(annotation.value()) ? databaseConfigurations.getDatabaseConfiguration().getDatabaseName() : annotation.value(); if (tempDatabaseName.equals(databaseName)) { ReflectionUtils.setFieldValue(testObject, field, dataSource); } } for (Method method : methods) { TestDataSource annotation = method.getAnnotation(TestDataSource.class); if (annotation == null) { continue; } String tempDatabaseName = StringUtils.isEmpty(annotation.value()) ? databaseConfigurations.getDatabaseConfiguration().getDatabaseName() : annotation.value(); if (tempDatabaseName.equals(databaseName)) { try { method.invoke(testObject, dataSource); } catch (IllegalAccessException ex) { logger.error(ex.getMessage(), ex); } catch (IllegalArgumentException ex) { logger.error(ex.getMessage(), ex); } catch (InvocationTargetException ex) { logger.error(ex.getMessage(), ex); } } } } protected DataSource getDataSource(String databaseName, Map mapDatasources, Object testObject) { DataSource datasource = null; if (mapDatasources.containsKey(databaseName)) { datasource = mapDatasources.get(databaseName); } else { DataSourceWrapper wrapper = getWrapper(databaseName); datasource = wrapper.getTransactionalDataSourceAndActivateTransactionIfNeeded(testObject); mapDatasources.put(databaseName, datasource); } return datasource; } /** * @param testObject * The test object, not null * @param testMethod * The test method, not null * @return The {@link TransactionMode} for the given object */ protected TransactionMode getTransactionMode(Object testObject, Method testMethod) { TransactionMode transactionMode = getMethodOrClassLevelAnnotationProperty(Transactional.class, "value", DEFAULT, testMethod, testObject.getClass()); transactionMode = getEnumValueReplaceDefault(Transactional.class, "value", transactionMode, defaultAnnotationPropertyValues); return transactionMode; } /** * Starts a transaction. If the Unitils DataSource was not loaded yet, we * simply remember that a transaction was started but don't actually start * it. If the DataSource is loaded within this test, the transaction will be * started immediately after loading the DataSource. * * @param testObject * The test object, not null * @param testMethod * The test method, not null */ protected void startTransactionForTestMethod(Object testObject, Method testMethod) { if (isTransactionsEnabled(testObject, testMethod)) { startTransaction(testObject); } } /** * Commits or rollbacks the current transaction, if transactions are enabled * and a transactionManager is active for the given testObject * * @param testObject * The test object, not null * @param testMethod * The test method, not null */ protected void endTransactionForTestMethod(Object testObject, Method testMethod) { if (isTransactionsEnabled(testObject, testMethod)) { if (getTransactionMode(testObject, testMethod) == COMMIT) { commitTransaction(testObject); } else if (getTransactionMode(testObject, testMethod) == ROLLBACK) { rollbackTransaction(testObject); } } } /** * Starts a new transaction on the transaction manager configured in unitils * * @param testObject * The test object, not null */ public void startTransaction(Object testObject) { getTransactionManager().startTransaction(testObject); } /** * Commits the current transaction. * * @param testObject * The test object, not null */ public void commitTransaction(Object testObject) { flushDatabaseUpdates(testObject); UnitilsTransactionManager transactionManager2 = getTransactionManager(); transactionManager2.activateTransactionIfNeeded(testObject); transactionManager2.commit(testObject); } /** * Performs a rollback of the current transaction * * @param testObject * The test object, not null */ public void rollbackTransaction(Object testObject) { flushDatabaseUpdates(testObject); UnitilsTransactionManager transactionManager2 = getTransactionManager(); transactionManager2.activateTransactionIfNeeded(testObject); transactionManager2.rollback(testObject); } /** * @param testObject * The test object, not null * @param testMethod * The test method, not null * @return Whether transactions are enabled for the given test method and * test object */ public boolean isTransactionsEnabled(Object testObject, Method testMethod) { TransactionMode transactionMode = getTransactionMode(testObject, testMethod); return transactionMode != DISABLED; } // todo javadoc public void registerTransactionManagementConfiguration(UnitilsTransactionManagementConfiguration transactionManagementConfiguration) { transactionManagementConfigurations.add(transactionManagementConfiguration); } protected Object getTestObject() { return Unitils.getInstance().getTestContext().getTestObject(); } /** * @return The {@link TestListener} associated with this module */ @Override public TestListener getTestListener() { return new DatabaseTestListener(); } /** * The {@link TestListener} for this module */ protected class DatabaseTestListener extends TestListener { @Override public void beforeTestSetUp(Object testObject, Method testMethod) { List databaseNames = databaseConfigurations.getDatabaseNames(); if (!databaseNames.isEmpty()) { for (String databaseName : databaseNames) { DataSourceWrapper wrapper = getWrapper(databaseName); setWrapper(wrapper); } } else { // register default wrapper getWrapper(""); } try { injectDataSource(testObject); } catch (Exception e) { throw new UnitilsException(e.getMessage(), e); } startTransactionForTestMethod(testObject, testMethod); } @Override public void afterTestTearDown(Object testObject, Method testMethod) { endTransactionForTestMethod(testObject, testMethod); } } /** * @return the wrapper */ public DataSourceWrapper getWrapper(String databaseName) { String tempDatabaseName = StringUtils.isEmpty(databaseName) ? databaseConfigurations.getDatabaseConfiguration().getDatabaseName() : databaseName; if (wrappers.containsKey(tempDatabaseName)) { return wrappers.get(tempDatabaseName); } DataSourceWrapper wrapper = null; if (StringUtils.isEmpty(databaseName)) { wrapper = new DataSourceWrapper(databaseConfigurations.getDatabaseConfiguration(), configuration, getTransactionManager()); } else { wrapper = new DataSourceWrapper(databaseConfigurations.getDatabaseConfiguration(databaseName), configuration, getTransactionManager()); } setWrapper(wrapper); return wrapper; } /** * @param wrapper * the wrapper to set */ public void setWrapper(DataSourceWrapper wrapper) { if (!wrappers.keySet().contains(wrapper.getDatabaseName())) { wrappers.put(wrapper.getDatabaseName(), wrapper); registerTransactionManagementConfiguration(wrapper); if (PropertyUtils.getBoolean(PROPERTY_RESET_SEQUENCES, false, configuration)) { wrapper.restartSequences(); } } } /** * @return the databaseConfigurations */ public DatabaseConfigurations getDatabaseConfigurations() { return databaseConfigurations; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy