/**
* Copyright (C) 2011-2017 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.flywaydb.test.dbunit;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dbunit.database.AmbiguousTableNameException;
import org.dbunit.database.DatabaseSequenceFilter;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.FilteredDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.filter.ITableFilter;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.flywaydb.test.ExecutionListenerHelper;
import org.springframework.test.context.support.AbstractTestExecutionListener;
/**
* A {@link TestExecutionListeners} to get the annotation {@link DBUnitSupport}
* up and running.</p>
*
* <b>Attention: at the moment this implementation is only tested with H2 and
* Oracle!</B></p>
*
* The annotation {@link DBUnitSupport}
* <ul>
* <li>{@link DBUnitSupport#loadFilesForRun()} use this with the DBUinit
* database operation and the load file (classpath ressource)</li>
* <li> {@link DBUnitSupport#saveFileAfterRun()} to store the result after run.
* if {@link DBUnitSupport#saveTableAfterRun()} is not omitted a whole database
* table export will be done.</br> Otherwise only the given tables will be
* exported.</li>
* </ul>
* Important if the annotation
* {@link org.flywaydb.test.annotation.FlywayTest} are used the system
* properties for <code>jdbc.url</code>, <code>jdbc.driver</code>,
* <code>jdbc.username</code>, and <code>jdbc.password</code> should be set.</p>
* Also the test application context should contains code like this:
*
* <pre>
* {@code
* <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
* <property name="location"><value>jdbc.properties</value></property>
* <property name="ignoreResourceNotFound" value="true"/>
* </bean>
*
* <!-- H2 Setup
* -Djdbc.driver=org.h2.Driver
* -Djdbc.url=jdbc:h2:./db/testCaseDb
* -Djdbc.username=OC_MORE_TEST
* -Djdbc.password=OC_MORE_TEST
*
* -->
* <bean id="dataSourceId" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
* <property name="driverClassName"><value>$jdbc.driver</value></property>
* <property name="url"><value>$jdbc.url</value></property>
* <property name="username"><value>$jdbc.username</value></property>
* <property name="password"><value>$jdbc.password</value></property>
* </bean>
* }
* </PRE>
*
* </br> If this setup is used exist the possibility to run test again different
* database such as H2 or Oracle.
* <p/>
* Usage inside the TestClass
*
* <pre>
* @RunWith(SpringJUnit4ClassRunner.class)
* @ContextConfiguration(locations = { "/context/simple_applicationContext.xml" })
* @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
* DBUnitTestExecutionListener.class })
* public class SimpleLoadTest {
*
* @Test
* @DBUnitSupport(saveFileAfterRun = "./dbunit/result/oneMandatorCreate.xml", saveTableAfterRun = {
* "MANDATOR", "" })
* public void oneMandatorCreate() throws SQLException
* {
* ...
* }
*
* @Test
* @FlywayTest
* @DBUnitSupport(loadFilesForRun={"INSERT","dbunit/insert/oneMandatorInsert.xml"})
* public void oneMandatorInsert() throws SQLException
* {
* ....
* }
* }
* </pre>
*
* If the {@link DefaultDatabaseConnectionFactory} do not fit for usage with DBunit it can be customized.<br/>
* Define a bean in the application context that implement the interface {@link DatabaseConnectionFactory}.
* Afterwards the own implementation of the database connection factory will be used for generating database connection.
* <p/>
*
* @author Florian Eska
*
* @date 2011-12-10
* @version 1.7
*
*/
public class DBUnitTestExecutionListener
extends AbstractTestExecutionListener
implements TestExecutionListener {
// @@ Construction
protected final Log logger = LogFactory.getLog(getClass());
@Autowired(required = false)
protected DatabaseConnectionFactory dbConnectionFactory = new DefaultDatabaseConnectionFactory();
/** default order 4500 */
private int order = 4500;
/**
* Allocates new <code>AbstractDbSpringContextTests</code> instance.
*/
public DBUnitTestExecutionListener() {
}
/**
* Only annotation with loadFiles will be executed
*/
public void beforeTestClass(final TestContext testContext) throws Exception {
// no we check for the DBResetForClass
final Class<?> testClass = testContext.getTestClass();
// For convinience one may annotate tests superclass.
final Annotation annotation = AnnotationUtils.findAnnotation(testClass,DBUnitSupport.class);
if (annotation != null) {
final DBUnitSupport dbUnitAnnotaton = (DBUnitSupport) annotation;
loadFiles(testContext, dbUnitAnnotaton);
}
}
public void prepareTestInstance(final TestContext testContext)
throws Exception {
}
/**
* Only annotation with loadFiles will be executed
*/
public void beforeTestMethod(final TestContext testContext)
throws Exception {
// no we check for the DBResetForClass
final Method testMethod = testContext.getTestMethod();
final Annotation annotation = testMethod
.getAnnotation(DBUnitSupport.class);
if (annotation != null) {
final DBUnitSupport dbUnitAnnotaton = (DBUnitSupport) annotation;
loadFiles(testContext, dbUnitAnnotaton);
}
}
/**
* Only annotation with saveIt will be executed
*/
public void afterTestMethod(final TestContext testContext) throws Exception {
// no we check for the DBResetForClass
final Method testMethod = testContext.getTestMethod();
final Annotation annotation = testMethod
.getAnnotation(DBUnitSupport.class);
String executionInfo = ExecutionListenerHelper
.getExecutionInformation(testContext);
if (annotation != null) {
final DBUnitSupport dbUnitAnnotaton = (DBUnitSupport) annotation;
final String saveIt = dbUnitAnnotaton.saveFileAfterRun();
final String[] tables = dbUnitAnnotaton.saveTableAfterRun();
if (saveIt != null && saveIt.trim().length() > 0) {
if (logger.isDebugEnabled()) {
logger.debug("******** Start save information '"
+ executionInfo + "' info file '" + saveIt + "'.");
}
final DataSource ds = getSaveDataSource(testContext);
final IDatabaseConnection con = getConnection(ds, testContext);
// Issue 16 fix leaking database connection - will look better with Java 7 Closable
try {
final IDataSet dataSet = getDataSetToExport(tables, con);
final File fileToExport = getFileToExport(saveIt);
final FileWriter fileWriter = new FileWriter(fileToExport);
FlatXmlDataSet.write(dataSet, fileWriter);
} finally {
if (logger.isDebugEnabled()) {
logger.debug("******** Close database connection " + con);
}
con.close();
}
if (logger.isDebugEnabled()) {
logger.debug("******** Finished save information '"
+ executionInfo + "' info file '" + saveIt + "'.");
}
}
}
}
public void afterTestClass(final TestContext testContext) throws Exception {
}
/**
* Implementation of loadFiles
*
* @param testContext
* @param dbUnitAnnotation
* @throws Exception
*/
private void loadFiles(final TestContext testContext,
final DBUnitSupport dbUnitAnnotation) throws Exception {
final String[] loadFiles = dbUnitAnnotation.loadFilesForRun();
if (loadFiles != null && loadFiles.length > 0) {
// we have some files to load
String executionInfo = ExecutionListenerHelper
.getExecutionInformation(testContext);
if (logger.isDebugEnabled()) {
logger.debug("******** Load files '" + executionInfo + "'.");
}
for (int i = 0; i < loadFiles.length; i += 2) {
final String operationName = loadFiles[i];
final String fileResource = loadFiles[i + 1];
if (logger.isDebugEnabled()) {
logger.debug("******** load file '" + executionInfo
+ "' op='" + operationName + "' - '" + fileResource
+ "'.");
}
final DatabaseOperation operation = getOperation(operationName);
final ClassPathResource resource = new ClassPathResource(
fileResource);
final InputStream is = resource.getInputStream();
try {
// now we try to load the data into database
final DataSource ds = getSaveDataSource(testContext);
final IDatabaseConnection con = getConnection(ds, testContext);
// Issue 16 fix leaking database connection - will look better with Java 7 Closable
try {
final FlatXmlDataSet dataSet = getFileDataSet(is);
operation.execute(con, dataSet);
} finally {
if (logger.isDebugEnabled()) {
logger.debug("******** Close database connection " + con);
}
con.close();
}
} finally {
if ( is != null ) {
// avoid memory leak in streams
is.close();
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("******** Finished load files '" + executionInfo
+ "'.");
}
}
}
private DatabaseOperation getOperation(final String operation)
throws SecurityException, NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
final String upOper = operation.toUpperCase();
final Field field = DatabaseOperation.class.getField(upOper);
if (field == null || !field.getType().equals(DatabaseOperation.class)) {
throw new IllegalArgumentException("Operation " + operation
+ " is unknown");
}
final DatabaseOperation result = (DatabaseOperation) field
.get(DatabaseOperation.class);
return result;
}
/**
* Get the a file to export. If the directory does not exist-> create it.
*
* @param fileName
* for the export file
* @return a file
*/
private File getFileToExport(final String fileName) {
String fileToExportName = fileName;
if (fileName.startsWith(".")) {
final File curDir = new File(".");
fileToExportName = curDir.getAbsolutePath() + File.separator
+ fileName;
}
final File fileToExport = new File(fileToExportName);
fileToExport.getParentFile().mkdirs();
return fileToExport;
}
/**
* Return a data set for export. A sort filter is used for the foreign key.
*
* @param tables
* which table should be exported.
* @param con
* is used for the export
*
* @return the export data set
*
* @throws DataSetException
* @throws SQLException
* @throws AmbiguousTableNameException
*/
private IDataSet getDataSetToExport(final String[] tables,
final IDatabaseConnection con) throws DataSetException,
SQLException, AmbiguousTableNameException {
final ITableFilter filter = new DatabaseSequenceFilter(con);
IDataSet dataSetToExport = null;
if (tables == null || tables.length == 0) {
// want a complete database export
dataSetToExport = con.createDataSet();
dataSetToExport = new FilteredDataSet(filter, dataSetToExport);
} else {
final QueryDataSet qDataSet = new QueryDataSet(con);
// generate the data set
if (tables.length % 2 != 0) {
throw new IllegalArgumentException(
"Contract {<Table Name>,<SELECT_QUERY>} is brocken.");
}
for (int i = 0; i < tables.length; i += 2) {
final String table = tables[i].toUpperCase();
String query = tables[i + 1];
if (query == null || query.trim().length() == 0) {
query = "SELECT * FROM " + table;
}
qDataSet.addTable(table, query);
}
dataSetToExport = qDataSet;
}
return dataSetToExport;
}
/**
*
* @param testContext
* @return the data source to use
*/
private DataSource getSaveDataSource(final TestContext testContext) {
final ApplicationContext appContext = testContext
.getApplicationContext();
if (appContext != null) {
final DataSource dataSource = getBean(appContext, DataSource.class);
if (dataSource != null) {
return dataSource;
}
throw new IllegalArgumentException(
"The test application context has no configured data source!");
}
throw new IllegalArgumentException(
"The test configuration contains no application context.");
}
/**
* Get the dbunit specific database connection
*
* @param dataSource
* @return
* @throws Exception
*/
protected IDatabaseConnection getConnection(final DataSource dataSource,final TestContext context)
throws Exception {
// get connection
final Connection con = dataSource.getConnection();
final DatabaseMetaData databaseMetaData = con.getMetaData();
IDatabaseConnection connection = null;
try {
DatabaseConnectionFactory factory = context.getApplicationContext().getBean(DatabaseConnectionFactory.class);
if ( factory != null )
{
dbConnectionFactory = factory;
}
}
catch ( Exception e)
{
logger.debug(String.format("We ignore if we could not find a instance of '%s'",DatabaseConnectionFactory.class.getName()));
}
if ( dbConnectionFactory != null ) {
connection = dbConnectionFactory.createConnection(con, databaseMetaData);
return connection;
}
// else {
// //nnneee
// DatabaseConnectionFactory factory = context.getApplicationContext().getBean(DatabaseConnectionFactory.class);
// dbConnectionFactory = factory;
// connection = dbConnectionFactory.createConnection(con, databaseMetaData);
// return connection;
// }
return null;
}
private FlatXmlDataSet getFileDataSet(final InputStream is)
throws Exception {
final FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
return builder.build(is);
}
/**
* Wrapper to get a methode
* <code>ApplicationContext.getBean(Class _class)</code> like in spring 3.0.
*
* @param context
* from which the bean should be retrieved
* @param classType
* class type
*
* @return a object of the type or <code>null</code>
*/
private DataSource getBean(final ApplicationContext context,
final Class<?> classType) {
DataSource result = null;
String[] names = context.getBeanNamesForType(classType);
if (names != null && names.length > 0) {
// we always return the bean with the first name
result = (DataSource) context.getBean(names[0]);
}
return result;
}
/**
* change the default order value;
* @since 3.2.1.1
*
*/
public void setOrder(int order) {
this.order = order;
}
/**
*
* @return order default 4500
* @since 3.2.1.1
*
*/
public int getOrder() {
return order;
}
}