package liquibase.integration.spring; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.sql.Connection; import java.sql.SQLException; import java.util.Enumeration; import java.util.Map; import java.util.Vector; import javax.sql.DataSource; 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.logging.LogFactory; import liquibase.logging.Logger; import liquibase.resource.ResourceAccessor; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; /** * A Spring-ified wrapper for Liquibase. * * Example Configuration: * <p> * <p> * This Spring configuration example will cause liquibase to run * automatically when the Spring context is initialized. It will load * <code>db-changelog.xml</code> from the classpath and apply it against * <code>myDataSource</code>. * <p> * * <pre> * <bean id="myLiquibase" * class="liquibase.spring.SpringLiquibase" * > * * <property name="dataSource" ref="myDataSource" /> * * <property name="changeLog" value="classpath:db-changelog.xml" /> * * <!-- The following configuration options are optional --> * * <property name="executeEnabled" value="true" /> * * <!-- * If set to true, writeSqlFileEnabled will write the generated * SQL to a file before executing it. * --> * <property name="writeSqlFileEnabled" value="true" /> * * <!-- * sqlOutputDir specifies the directory into which the SQL file * will be written, if so configured. * --> * <property name="sqlOutputDir" value="c:\sql" /> * * </bean> * * </pre> * * @author Rob Schoening */ public class SpringLiquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware { public class SpringResourceOpener implements ResourceAccessor { private String parentFile; public SpringResourceOpener(String parentFile) { this.parentFile = parentFile; } public InputStream getResourceAsStream(String file) throws IOException { try { Resource resource = getResource(file); return resource.getInputStream(); } catch ( FileNotFoundException ex ) { return null; } } public Enumeration<URL> getResources(String packageName) throws IOException { Vector<URL> tmp = new Vector<URL>(); tmp.add(getResource(packageName).getURL()); return tmp.elements(); } public Resource getResource(String file) { return getResourceLoader().getResource(adjustClasspath(file)); } private String adjustClasspath(String file) { return isClasspathPrefixPresent(parentFile) && !isClasspathPrefixPresent(file) ? ResourceLoader.CLASSPATH_URL_PREFIX + file : file; } public boolean isClasspathPrefixPresent(String file) { return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX); } public ClassLoader toClassLoader() { return getResourceLoader().getClassLoader(); } } private String beanName; private ResourceLoader resourceLoader; private DataSource dataSource; private Logger log = LogFactory.getLogger(SpringLiquibase.class.getName()); private String changeLog; private String contexts; private Map<String, String> parameters; private String defaultSchema; public SpringLiquibase() { super(); } public String getDatabaseProductName() throws DatabaseException { Connection connection = null; String name = "unknown"; try { connection = getDataSource().getConnection(); Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())); name = database.getDatabaseProductName(); } catch (SQLException e) { throw new DatabaseException(e); } finally { if (connection != null) { try { if (!connection.getAutoCommit()) { connection.rollback(); } connection.close(); } catch (Exception e) { log.warning("problem closing connection", e); } } } return name; } /** * The DataSource that liquibase will use to perform the migration. * * @return */ public DataSource getDataSource() { return dataSource; } /** * The DataSource that liquibase will use to perform the migration. */ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * Returns a Resource that is able to resolve to a file or classpath resource. * * @return */ public String getChangeLog() { return changeLog; } /** * Sets a Spring Resource that is able to resolve to a file or classpath resource. * An example might be <code>classpath:db-changelog.xml</code>. */ public void setChangeLog(String dataModel) { this.changeLog = dataModel; } public String getContexts() { return contexts; } public void setContexts(String contexts) { this.contexts = contexts; } public String getDefaultSchema() { return defaultSchema; } public void setDefaultSchema(String defaultSchema) { this.defaultSchema = defaultSchema; } /** * Executed automatically when the bean is initialized. */ public void afterPropertiesSet() throws LiquibaseException { String shouldRunProperty = System.getProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY); if (shouldRunProperty != null && !Boolean.valueOf(shouldRunProperty)) { System.out.println("Liquibase did not run because '" + Liquibase.SHOULD_RUN_SYSTEM_PROPERTY + "' system property was set to false"); return; } Connection c = null; Liquibase liquibase = null; try { c = getDataSource().getConnection(); liquibase = createLiquibase(c); liquibase.update(getContexts()); } catch (SQLException e) { throw new DatabaseException(e); } finally { if (liquibase != null) { liquibase.forceReleaseLocks(); } 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(getChangeLog(), createResourceOpener(), createDatabase(c)); if (parameters != null) { for(Map.Entry<String, String> entry: parameters.entrySet()) { liquibase.setChangeLogParameter(entry.getKey(), entry.getValue()); } } 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 DatabaseFactory}. * @throws DatabaseException */ protected Database createDatabase(Connection c) throws DatabaseException { Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(c)); if (this.defaultSchema != null) { database.setDefaultSchemaName(this.defaultSchema); } return database; } public void setChangeLogParameters(Map<String, String> parameters) { this.parameters = parameters; } /** * Create a new resourceOpener. */ protected SpringResourceOpener createResourceOpener() { return new SpringResourceOpener(getChangeLog()); } /** * Spring sets this automatically to the instance's configured bean name. */ public void setBeanName(String name) { this.beanName = name; } /** * Gets the Spring-name of this instance. * * @return */ public String getBeanName() { return beanName; } public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } public ResourceLoader getResourceLoader() { return resourceLoader; } @Override public String toString() { return getClass().getName()+"("+this.getResourceLoader().toString()+")"; } }