/**
* 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.junit;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.flywaydb.core.Flyway;
import org.flywaydb.test.ExecutionListenerHelper;
import org.flywaydb.test.annotation.FlywayTest;
import org.springframework.test.context.support.AbstractTestExecutionListener;
/**
* Spring test execution listener to get annotation {@link FlywayTest} up and
* running
*
* <p>
* If the annotation {@link FlywayTest} used on class level a clean , init ,
* migrate cycle is done during class load.<br>
* If the annotation {@link FlywayTest} used on test method level a clean , init
* , migrate cycle is done before test execution.
* </p>
*
* <p>
* Important if the annotation {@link 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="dbc.properties"/>
* <property name="ignoreResourceNotFound" value="true"/>
* </bean>
*
* <bean id="flyway" class="org.flywaydb.core.Flyway" depends-on="data.source.id">
* <property name="dataSource" ref="data.source.id"/>
* <property name="locations" value="oracle"/>
* </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="dataSourceRef" 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>
*
* If this setup is used exist the possibility to run test again different
* database such as H2 or Oracle.
* <p/>
* Usage inside the test class
*
* <pre>
* @RunWith(SpringJUnit4ClassRunner.class)
* @ContextConfiguration(locations={"/context/simple_applicationContext.xml"})
* @TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
* FlywayTestExecutionListener.class})
* @FlywayTest
* public class SimpleLoadTest
* </pre>
*
* </p>
* <b>Notes:</b>
* <ul>
* <li>If you using spring framework version lower than 3.x the annotation
* {@link FlywayTest} wont work at class level.</li>
* <li>For spring framework version 2.5.6 use
* simple_applicationContext_spring256.xml as application context example</li>
* <li>If you using the annotation {@link FlywayTest} more than one time in test
* classes than <b>do not</b> use parallel execution in surefire plugin.</bR>
* With this option you will setup your database in more than one thread
* parallel!</li>
* </ul>
*
* </p>
*
* @author Florian
*
* @version 2011-12-10
* @version 1.7
*
*/
public class FlywayTestExecutionListener
extends AbstractTestExecutionListener
implements TestExecutionListener {
/**
* Used for logging inside test executions.
*/
// @@ Construction
private final Log logger = LogFactory.getLog(getClass());
/** default order 4000 */
private int order = 4000;
/**
* Allocates new <code>AbstractDbSpringContextTests</code> instance.
*/
public FlywayTestExecutionListener() {
}
/**
* @return the instance of logger.
*/
protected Log getLogger() {
return logger;
}
/**
* Invoke this method before test class will be created.</p>
*
* <b>Attention:</b> This will be only invoked if spring version >= 3.x
* are used.
*
* @param testContext
* default test context filled from spring
*
* @throws Exception
* if any error occurred
*/
public void beforeTestClass(final TestContext testContext) throws Exception {
// no we check for the DBResetForClass
final Class<?> testClass = testContext.getTestClass();
final Annotation annotation = AnnotationUtils.findAnnotation(testClass, FlywayTest.class);
dbResetWithAnotation(testContext, (FlywayTest) annotation);
}
/**
* no implementation for annotation {@link FlywayTest} needed.
*
* @param testContext
* default test context filled from spring
*
* @throws Exception
* if any error occurred
*/
public void prepareTestInstance(final TestContext testContext)
throws Exception {
}
/**
* Called from spring before a test method will be invoked.
*
* @param testContext
* default test context filled from spring
*
* @throws Exception
* if any error occurred
*/
public void beforeTestMethod(final TestContext testContext)
throws Exception {
final Method testMethod = testContext.getTestMethod();
final Annotation annotation = testMethod
.getAnnotation(FlywayTest.class);
dbResetWithAnotation(testContext, (FlywayTest) annotation);
}
/**
* no implementation for annotation {@link FlywayTest} needed.
*
* @param testContext
* default test context filled from spring
*
* @throws Exception
* if any error occurred
*/
public void afterTestMethod(final TestContext testContext) throws Exception {
}
/**
* no implementation for annotation {@link FlywayTest} needed.
*
* @param testContext
* default test context filled from spring
*
* @throws Exception
* if any error occurred
*/
public void afterTestClass(final TestContext testContext) throws Exception {
}
/**
* Test the annotation an reset the database.
*
* @param testContext
* default test context filled from spring
* @param annotation
* founded
*/
private void dbResetWithAnotation(final TestContext testContext,
final FlywayTest annotation) {
if (annotation != null) {
Flyway flyWay = null;
final ApplicationContext appContext = testContext
.getApplicationContext();
final String executionInfo = ExecutionListenerHelper
.getExecutionInformation(testContext);
if (appContext != null) {
flyWay = getBean(appContext, Flyway.class, annotation.flywayName());
if (flyWay != null) {
// we have a fly way configuration no lets try
if (logger.isInfoEnabled()) {
logger.info("---> Start reset database for '"
+ executionInfo + "'.");
}
if (annotation.invokeCleanDB()) {
if (logger.isDebugEnabled()) {
logger.debug("******** Clean database for '"
+ executionInfo + "'.");
}
flyWay.clean();
}
if (annotation.invokeBaselineDB()) {
if (logger.isDebugEnabled()) {
logger.debug("******** Baseline database for '"
+ executionInfo + "'.");
}
flyWay.baseline();
}
if (annotation.invokeMigrateDB()) {
String[] locations = annotation.locationsForMigrate();
if ((locations == null || locations.length == 0)) {
if (logger.isDebugEnabled()) {
logger.debug("******** Default migrate database for '"
+ executionInfo + "'.");
}
flyWay.migrate();
} else {
locationsMigrationHandling(annotation, flyWay,
executionInfo);
}
}
if (logger.isInfoEnabled()) {
logger.info("<--- Finished reset database for '"
+ executionInfo + "'.");
}
return;
}
// in this case we have not the possibility to reset the
// database
throw new IllegalArgumentException("Annotation "
+ annotation.getClass()
+ " was set, but no Flyway configuration was given.");
}
// in this case we have not the possibility to reset the database
throw new IllegalArgumentException("Annotation "
+ annotation.getClass()
+ " was set, but no configuration was given.");
}
}
/**
* Handling of the change of locations configuration of a flyway.
*
* @param annotation
* current annotation
* @param flyWay
* bean
* @param executionInfo
* current test context.
*/
private void locationsMigrationHandling(final FlywayTest annotation,
final Flyway flyWay, final String executionInfo) {
final String[] locations = annotation.locationsForMigrate();
// now migration handling for locations support
String[] oldLocations = flyWay.getLocations();
boolean override = annotation.overrideLocations();
try {
String[] useLocations = null;
if (override) {
useLocations = locations;
} else {
// Fill the locations
useLocations = Arrays.copyOf(oldLocations, oldLocations.length
+ locations.length);
System.arraycopy(locations, 0, useLocations, oldLocations.length, locations.length);
}
if (logger.isDebugEnabled()) {
logger.debug(String
.format("******** Start migration from locations directories '%s' for '%s'.",
Arrays.asList(useLocations), executionInfo));
}
flyWay.setLocations(useLocations);
flyWay.migrate();
} finally {
// reset the flyway bean to original configuration.
flyWay.setLocations(oldLocations);
}
}
/**
* Wrapper to get a method
* <code>ApplicationContext.getBean(Class _class)</code> like in spring 3.0.
* It will returns always the first instance of the founded class.
*
* @param context
* from which the bean should be retrieved
* @param classType
* class type that should be retrieved from the configuration
* file.
*
* @return a object of the type or <code>null</code>
*/
private Flyway getBean(final ApplicationContext context,
final Class<?> classType, String idName) {
Flyway result = null;
String[] names = context.getBeanNamesForType(classType);
if (names != null && names.length > 0) {
if ( idName == null || idName.trim().isEmpty() ) {
// old behaviour
// we always return the bean with the first name
result = (Flyway) context.getBean(names[0]);
} else {
result = (Flyway) context.getBean(idName);
}
}
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;
}
}