Permalink
Browse files

Support for running a Liquibase update on startup of a CDI container

  • Loading branch information...
1 parent 8adb926 commit 5be42a6779b2bef95e2d02d8703aceec91aedb59 @aaronwalker committed Jan 29, 2012
View
18 liquibase-core/pom.xml
@@ -43,8 +43,26 @@
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>javax.enterprise</groupId>
+ <artifactId>cdi-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.jboss.weld.se</groupId>
+ <artifactId>weld-se</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ </dependency>
+
</dependencies>
+
+
<build>
<finalName>liquibase-${project.version}</finalName>
View
106 liquibase-core/src/main/java/liquibase/integration/cdi/CDIBootstrap.java
@@ -0,0 +1,106 @@
+package liquibase.integration.cdi;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.spi.CreationalContext;
+import javax.enterprise.event.Observes;
+import javax.enterprise.inject.Any;
+import javax.enterprise.inject.Default;
+import javax.enterprise.inject.spi.AfterBeanDiscovery;
+import javax.enterprise.inject.spi.AfterDeploymentValidation;
+import javax.enterprise.inject.spi.AnnotatedType;
+import javax.enterprise.inject.spi.Bean;
+import javax.enterprise.inject.spi.BeanManager;
+import javax.enterprise.inject.spi.Extension;
+import javax.enterprise.inject.spi.InjectionPoint;
+import javax.enterprise.inject.spi.InjectionTarget;
+import javax.enterprise.util.AnnotationLiteral;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * CDI Bootstrap
+ * <p/>
+ * Observes CDI container startup events and triggers the Liquibase update
+ * process via @PostConstruct on CDILiquibase
+ *
+ * @author Aaron Walker (http://github.com/aaronwalker)
+ */
+public class CDIBootstrap implements Extension {
+
+ private Bean<CDILiquibase> instance;
+
+ void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
+ AnnotatedType<CDILiquibase> at = bm.createAnnotatedType(CDILiquibase.class);
+ final InjectionTarget<CDILiquibase> it = bm.createInjectionTarget(at);
+ instance = new Bean<CDILiquibase>() {
+
+ public Set<Type> getTypes() {
+ Set<Type> types = new HashSet<Type>();
+ types.add(CDILiquibase.class);
+ types.add(Object.class);
+ return types;
+ }
+
+ public Set<Annotation> getQualifiers() {
+ Set<Annotation> qualifiers = new HashSet<Annotation>();
+ qualifiers.add( new AnnotationLiteral<Default>() {} );
+ qualifiers.add( new AnnotationLiteral<Any>() {} );
+ return qualifiers;
+ }
+
+ public Class<? extends Annotation> getScope() {
+ return ApplicationScoped.class;
+ }
+
+ public String getName() {
+ return "cdiLiquibase";
+ }
+
+ public Set<Class<? extends Annotation>> getStereotypes() {
+ return Collections.emptySet();
+ }
+
+ public Class<?> getBeanClass() {
+ return CDILiquibase.class;
+ }
+
+ public boolean isAlternative() {
+ return false;
+ }
+
+ public boolean isNullable() {
+ return false;
+ }
+
+ public Set<InjectionPoint> getInjectionPoints() {
+ return it.getInjectionPoints();
+ }
+
+ public CDILiquibase create(CreationalContext<CDILiquibase> ctx) {
+ CDILiquibase instance = it.produce(ctx);
+ it.inject(instance, ctx);
+ it.postConstruct(instance);
+ return instance;
+ }
+
+ public void destroy(CDILiquibase instance, CreationalContext<CDILiquibase> ctx) {
+ it.preDestroy(instance);
+ it.dispose(instance);
+ ctx.release();
+ }
+ };
+ abd.addBean(instance);
+ }
+
+ void afterDeploymentValidation(@Observes AfterDeploymentValidation event, BeanManager manager) {
+ try {
+ manager.getReference(instance, instance.getBeanClass(), manager.createCreationalContext(instance)).toString();
+ } catch (Exception ex) {
+ event.addDeploymentProblem(ex);
+ }
+ }
+
+}
View
169 liquibase-core/src/main/java/liquibase/integration/cdi/CDILiquibase.java
@@ -0,0 +1,169 @@
+package liquibase.integration.cdi;
+
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.LiquibaseException;
+import liquibase.integration.cdi.annotations.LiquibaseType;
+import liquibase.logging.LogFactory;
+import liquibase.logging.Logger;
+import liquibase.resource.ResourceAccessor;
+import liquibase.util.LiquibaseUtil;
+import liquibase.util.NetUtil;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.spi.Extension;
+import javax.inject.Inject;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Map;
+
+/**
+ * A CDI wrapper for Liquibase.
+ * <p/>
+ * Example Configuration:
+ * <p/>
+ * <p/>
+ * This CDI configuration example will cause liquibase to run
+ * automatically when the CDI container is initialized. It will load
+ * <code>db-changelog.xml</code> from the classpath and apply it against
+ * <code>myDataSource</code>.
+ * <p/>
+ * Various producers methods are required to resolve the dependencies
+ * i.e.
+ * <code>
+ * public class CDILiquibaseProducer {
+ *
+ * @Produces @LiquibaseType
+ * public CDILiquibaseConfig createConfig() {
+ * CDILiquibaseConfig config = new CDILiquibaseConfig();
+ * config.setChangeLog("liquibase/parser/core/xml/simpleChangeLog.xml");
+ * return config;
+ * }
+ *
+ * @Produces @LiquibaseType
+ * public DataSource createDataSource() throws SQLException {
+ * jdbcDataSource ds = new jdbcDataSource();
+ * ds.setDatabase("jdbc:hsqldb:mem:test");
+ * ds.setUser("sa");
+ * ds.setPassword("");
+ * return ds;
+ * }
+ *
+ * @Produces @LiquibaseType
+ * public ResourceAccessor create() {
+ * return new ClassLoaderResourceAccessor(getClass().getClassLoader());
+ * }
+ *
+ * }
+ * </code>
+ *
+ * @author Aaron Walker (http://github.com/aaronwalker)
+ */
+@ApplicationScoped
+public class CDILiquibase implements Extension {
+
+ private Logger log = LogFactory.getLogger(CDILiquibase.class.getName());
+
+ @Inject @LiquibaseType
+ private CDILiquibaseConfig config;
+
+ @Inject @LiquibaseType
+ private DataSource dataSource;
+
+ @Inject @LiquibaseType
+ ResourceAccessor resourceAccessor;
+
+ private boolean initialized = false;
+ private boolean updateSuccessful = false;
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ public boolean isUpdateSuccessful() {
+ return updateSuccessful;
+ }
+
+ @PostConstruct
+ public void onStartup() throws LiquibaseException {
+ log.info("Booting Liquibase " + LiquibaseUtil.getBuildVersion());
+ String hostName;
+ try {
+ hostName = NetUtil.getLocalHost().getHostName();
+ } catch (Exception e) {
+ log.warning("Cannot find hostname: " + e.getMessage());
+ log.debug("", e);
+ return;
+ }
+
+ String shouldRunProperty = System.getProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY);
+ if (shouldRunProperty != null && !Boolean.valueOf(shouldRunProperty)) {
+ log.info("Liquibase did not run on " + hostName + " because '" + Liquibase.SHOULD_RUN_SYSTEM_PROPERTY + "' system property was set to false");
+ return;
+ }
+ initialized = true;
+ performUpdate();
+ }
+
+ private void performUpdate() throws LiquibaseException {
+ Connection c = null;
+ Liquibase liquibase = null;
+ try {
+ c = dataSource.getConnection();
+ liquibase = createLiquibase(c);
+ liquibase.update(config.getContexts());
+ updateSuccessful = true;
+ } catch (SQLException e) {
+ throw new DatabaseException(e);
+ } catch (LiquibaseException ex) {
+ updateSuccessful = false;
+ throw ex;
+ } finally {
+ if (c != null) {
+ try {
+ c.rollback();
+ c.close();
+ } catch (SQLException e) {
+ //nothing to do
+ }
+
+ }
+
+ }
+ }
+
+ protected Liquibase createLiquibase(Connection c) throws LiquibaseException {
+ Liquibase liquibase = new Liquibase(config.getChangeLog(), resourceAccessor, createDatabase(c));
+ if (config.getParameters() != null) {
+ for(Map.Entry<String, String> entry: config.getParameters().entrySet()) {
+ liquibase.setChangeLogParameter(entry.getKey(), entry.getValue());
+ }
+ }
+
+ if (config.isDropFirst()) {
+ liquibase.dropAll();
+ }
+
+ return liquibase;
+ }
+
+ /**
+ * Subclasses may override this method add change some database settings such as
+ * default schema before returning the database object.
+ * @param c
+ * @return a Database implementation retrieved from the {@link liquibase.database.DatabaseFactory}.
+ * @throws DatabaseException
+ */
+ protected Database createDatabase(Connection c) throws DatabaseException {
+ Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(c));
+ if (config.getDefaultSchema() != null) {
+ database.setDefaultSchemaName(config.getDefaultSchema());
+ }
+ return database;
+ }
+}
View
60 liquibase-core/src/main/java/liquibase/integration/cdi/CDILiquibaseConfig.java
@@ -0,0 +1,60 @@
+package liquibase.integration.cdi;
+
+import com.sun.xml.internal.ws.util.QNameMap;
+import liquibase.resource.ResourceAccessor;
+
+import java.util.Map;
+
+/**
+ * Holds the configuration for Liquibase
+ *
+ * @author Aaron Walker (http://github.com/aaronwalker)
+ */
+public class CDILiquibaseConfig {
+
+ private String contexts;
+ private String changeLog;
+ private Map<String,String> parameters;
+ private boolean dropFirst = false;
+ private String defaultSchema;
+
+ public String getContexts() {
+ return contexts;
+ }
+
+ public void setContexts(String contexts) {
+ this.contexts = contexts;
+ }
+
+ public String getChangeLog() {
+ return changeLog;
+ }
+
+ public void setChangeLog(String changeLog) {
+ this.changeLog = changeLog;
+ }
+
+ public Map<String, String> getParameters() {
+ return parameters;
+ }
+
+ public void setParameters(Map<String, String> parameters) {
+ this.parameters = parameters;
+ }
+
+ public boolean isDropFirst() {
+ return dropFirst;
+ }
+
+ public void setDropFirst(boolean dropFirst) {
+ this.dropFirst = dropFirst;
+ }
+
+ public String getDefaultSchema() {
+ return defaultSchema;
+ }
+
+ public void setDefaultSchema(String defaultSchema) {
+ this.defaultSchema = defaultSchema;
+ }
+}
View
22 liquibase-core/src/main/java/liquibase/integration/cdi/annotations/LiquibaseType.java
@@ -0,0 +1,22 @@
+package liquibase.integration.cdi.annotations;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.METHOD;
+
+import javax.inject.Qualifier;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Qualifier Annontation
+ *
+ * @author Aaron Walker (http://github.com/aaronwalker)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({FIELD,METHOD,PARAMETER,TYPE})
+@Qualifier
+public @interface LiquibaseType {
+}
View
1 liquibase-core/src/main/resources/META-INF/beans.xml
@@ -0,0 +1 @@
+<beans/>
View
1 liquibase-core/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension
@@ -0,0 +1 @@
+liquibase.integration.cdi.CDIBootstrap
View
41 liquibase-core/src/test/java/liquibase/integration/cdi/CDILiquibaseTest.java
@@ -0,0 +1,41 @@
+package liquibase.integration.cdi;
+
+import liquibase.Liquibase;
+import liquibase.integration.cdi.annotations.LiquibaseType;
+import org.jboss.weld.environment.se.Weld;
+import org.jboss.weld.environment.se.WeldContainer;
+import org.junit.Test;
+
+
+import javax.enterprise.util.AnnotationLiteral;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for CDILiquibaseTest
+ *
+ * @author Aaron Walker (http://github.com/aaronwalker)
+ */
+public class CDILiquibaseTest {
+
+
+ @Test
+ public void shouldntRunWhenShouldRunIsFalse() {
+ System.setProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY,"false");
+ WeldContainer weld = new Weld().initialize();
+ CDILiquibase cdiLiquibase = weld.instance().select(CDILiquibase.class).get();
+ assertNotNull(cdiLiquibase);
+ assertFalse(cdiLiquibase.isInitialized());
+ assertFalse(cdiLiquibase.isUpdateSuccessful());
+ }
+
+ @Test
+ public void shouldRunWhenShouldRunIsTrue() {
+ System.setProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY,"true");
+ WeldContainer weld = new Weld().initialize();
+ CDILiquibase cdiLiquibase = weld.instance().select(CDILiquibase.class).get();
+ assertNotNull(cdiLiquibase);
+ assertTrue(cdiLiquibase.isInitialized());
+ assertTrue(cdiLiquibase.isUpdateSuccessful());
+ }
+}
View
45 liquibase-core/src/test/java/liquibase/integration/cdi/CDITestProducer.java
@@ -0,0 +1,45 @@
+package liquibase.integration.cdi;
+
+import liquibase.integration.cdi.annotations.LiquibaseType;
+import liquibase.resource.ClassLoaderResourceAccessor;
+import liquibase.resource.ResourceAccessor;
+import org.hsqldb.jdbc.jdbcDataSource;
+import org.jboss.weld.resources.ClassLoaderResourceLoader;
+
+import javax.enterprise.inject.Produces;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.Enumeration;
+
+/**
+ * A Test CDI Producer used for testing CDILiquibase
+ *
+ * @author Aaron Walker (http://github.com/aaronwalker)
+ */
+public class CDITestProducer {
+
+ @Produces @LiquibaseType
+ public CDILiquibaseConfig createConfig() {
+ CDILiquibaseConfig config = new CDILiquibaseConfig();
+ config.setChangeLog("liquibase/parser/core/xml/simpleChangeLog.xml");
+ return config;
+ }
+
+ @Produces @LiquibaseType
+ public DataSource createDataSource() throws SQLException {
+ jdbcDataSource ds = new jdbcDataSource();
+ ds.setDatabase("jdbc:hsqldb:mem:test");
+ ds.setUser("sa");
+ ds.setPassword("");
+ return ds;
+ }
+
+ @Produces @LiquibaseType
+ public ResourceAccessor create() {
+ return new ClassLoaderResourceAccessor(getClass().getClassLoader());
+ }
+
+}
View
1 liquibase-core/src/test/resources/META-INF/beans.xml
@@ -0,0 +1 @@
+<beans/>
View
15 pom.xml
@@ -124,6 +124,21 @@
</dependency>
<dependency>
+ <groupId>javax.enterprise</groupId>
+ <artifactId>cdi-api</artifactId>
+ <version>1.0-SP4</version>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.jboss.weld.se</groupId>
+ <artifactId>weld-se</artifactId>
+ <version>1.1.5.Final</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-verifier</artifactId>
<version>1.2</version>

0 comments on commit 5be42a6

Please sign in to comment.