View Javadoc

1   package liquibase.integration.spring;
2   
3   import java.io.FileNotFoundException;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.net.URL;
7   import java.sql.Connection;
8   import java.sql.SQLException;
9   import java.util.Enumeration;
10  import java.util.Map;
11  import java.util.Vector;
12  
13  import javax.sql.DataSource;
14  
15  import liquibase.Liquibase;
16  import liquibase.database.Database;
17  import liquibase.database.DatabaseFactory;
18  import liquibase.database.jvm.JdbcConnection;
19  import liquibase.exception.DatabaseException;
20  import liquibase.exception.LiquibaseException;
21  import liquibase.logging.LogFactory;
22  import liquibase.logging.Logger;
23  import liquibase.resource.ResourceAccessor;
24  
25  import org.springframework.beans.factory.BeanNameAware;
26  import org.springframework.beans.factory.InitializingBean;
27  import org.springframework.context.ResourceLoaderAware;
28  import org.springframework.core.io.Resource;
29  import org.springframework.core.io.ResourceLoader;
30  
31  /**
32   * A Spring-ified wrapper for Liquibase.
33   * 
34   * Example Configuration:
35   * <p>
36   * <p>
37   * This Spring configuration example will cause liquibase to run automatically when the Spring context is initialized.
38   * It will load <code>db-changelog.xml</code> from the classpath and apply it against <code>myDataSource</code>.
39   * <p>
40   * 
41   * <pre>
42   * &lt;bean id=&quot;myLiquibase&quot;
43   *          class=&quot;liquibase.spring.SpringLiquibase&quot;
44   *          &gt;
45   * 
46   *      &lt;property name=&quot;dataSource&quot; ref=&quot;myDataSource&quot; /&gt;
47   * 
48   *      &lt;property name=&quot;changeLog&quot; value=&quot;classpath:db-changelog.xml&quot; /&gt;
49   * 
50   *      &lt;!-- The following configuration options are optional --&gt;
51   * 
52   *      &lt;property name=&quot;executeEnabled&quot; value=&quot;true&quot; /&gt;
53   * 
54   *      &lt;!--
55   *      If set to true, writeSqlFileEnabled will write the generated
56   *      SQL to a file before executing it.
57   *      --&gt;
58   *      &lt;property name=&quot;writeSqlFileEnabled&quot; value=&quot;true&quot; /&gt;
59   * 
60   *      &lt;!--
61   *      sqlOutputDir specifies the directory into which the SQL file
62   *      will be written, if so configured.
63   *      --&gt;
64   *      &lt;property name=&quot;sqlOutputDir&quot; value=&quot;c:\sql&quot; /&gt;
65   * 
66   * &lt;/bean&gt;
67   * 
68   * </pre>
69   * 
70   * @author Rob Schoening
71   */
72  public class SpringLiquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware {
73      public class SpringResourceOpener implements ResourceAccessor {
74          private String parentFile;
75  
76          public SpringResourceOpener(String parentFile) {
77              this.parentFile = parentFile;
78          }
79  
80          public InputStream getResourceAsStream(String file) throws IOException {
81              try {
82                  Resource resource = getResource(file);
83                  return resource.getInputStream();
84              } catch (FileNotFoundException ex) {
85                  return null;
86              }
87          }
88  
89          public Enumeration<URL> getResources(String packageName) throws IOException {
90              Vector<URL> tmp = new Vector<URL>();
91  
92              tmp.add(getResource(packageName).getURL());
93  
94              return tmp.elements();
95          }
96  
97          public Resource getResource(String file) {
98              return getResourceLoader().getResource(adjustClasspath(file));
99          }
100 
101         private String adjustClasspath(String file) {
102             return isClasspathPrefixPresent(parentFile) && !isClasspathPrefixPresent(file) ? ResourceLoader.CLASSPATH_URL_PREFIX
103                     + file
104                     : file;
105         }
106 
107         public boolean isClasspathPrefixPresent(String file) {
108             return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
109         }
110 
111         public ClassLoader toClassLoader() {
112             return getResourceLoader().getClassLoader();
113         }
114     }
115 
116     private String beanName;
117 
118     private ResourceLoader resourceLoader;
119 
120     private DataSource dataSource;
121 
122     private Logger log = LogFactory.getLogger(SpringLiquibase.class.getName());
123 
124     private String changeLog;
125 
126     private String contexts;
127 
128     private Map<String, String> parameters;
129 
130     private String defaultSchema;
131 
132     public SpringLiquibase() {
133         super();
134     }
135 
136     public String getDatabaseProductName() throws DatabaseException {
137         Connection connection = null;
138         String name = "unknown";
139         try {
140             connection = getDataSource().getConnection();
141             Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(
142                     new JdbcConnection(dataSource.getConnection()));
143             name = database.getDatabaseProductName();
144         } catch (SQLException e) {
145             throw new DatabaseException(e);
146         } finally {
147             if (connection != null) {
148                 try {
149                     if (!connection.getAutoCommit()) {
150                         connection.rollback();
151                     }
152                     connection.close();
153                 } catch (Exception e) {
154                     log.warning("problem closing connection", e);
155                 }
156             }
157         }
158         return name;
159     }
160 
161     /**
162      * The DataSource that liquibase will use to perform the migration.
163      * 
164      * @return
165      */
166     public DataSource getDataSource() {
167         return dataSource;
168     }
169 
170     /**
171      * The DataSource that liquibase will use to perform the migration.
172      */
173     public void setDataSource(DataSource dataSource) {
174         this.dataSource = dataSource;
175     }
176 
177     /**
178      * Returns a Resource that is able to resolve to a file or classpath resource.
179      * 
180      * @return
181      */
182     public String getChangeLog() {
183         return changeLog;
184     }
185 
186     /**
187      * Sets a Spring Resource that is able to resolve to a file or classpath resource. An example might be
188      * <code>classpath:db-changelog.xml</code>.
189      */
190     public void setChangeLog(String dataModel) {
191 
192         this.changeLog = dataModel;
193     }
194 
195     public String getContexts() {
196         return contexts;
197     }
198 
199     public void setContexts(String contexts) {
200         this.contexts = contexts;
201     }
202 
203     public String getDefaultSchema() {
204         return defaultSchema;
205     }
206 
207     public void setDefaultSchema(String defaultSchema) {
208         this.defaultSchema = defaultSchema;
209     }
210 
211     /**
212      * Executed automatically when the bean is initialized.
213      */
214     public void afterPropertiesSet() throws LiquibaseException {
215         String shouldRunProperty = System.getProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY);
216         if (shouldRunProperty != null && !Boolean.valueOf(shouldRunProperty)) {
217             System.out.println("Liquibase did not run because '" + Liquibase.SHOULD_RUN_SYSTEM_PROPERTY
218                     + "' system property was set to false");
219             return;
220         }
221 
222         Connection c = null;
223         Liquibase liquibase = null;
224         try {
225             c = getDataSource().getConnection();
226             liquibase = createLiquibase(c);
227             liquibase.update(getContexts());
228         } catch (SQLException e) {
229             throw new DatabaseException(e);
230         } finally {
231             if (liquibase != null) {
232                 liquibase.forceReleaseLocks();
233             }
234             if (c != null) {
235                 try {
236                     c.rollback();
237                     c.close();
238                 } catch (SQLException e) {
239                     // nothing to do
240                 }
241             }
242         }
243 
244     }
245 
246     protected Liquibase createLiquibase(Connection c) throws LiquibaseException {
247         Liquibase liquibase = new Liquibase(getChangeLog(), createResourceOpener(), createDatabase(c));
248         if (parameters != null) {
249             for (Map.Entry<String, String> entry : parameters.entrySet()) {
250                 liquibase.setChangeLogParameter(entry.getKey(), entry.getValue());
251             }
252         }
253 
254         return liquibase;
255     }
256 
257     /**
258      * Subclasses may override this method add change some database settings such as default schema before returning the
259      * database object.
260      * 
261      * @param c
262      * @return a Database implementation retrieved from the {@link DatabaseFactory}.
263      * @throws DatabaseException
264      */
265     protected Database createDatabase(Connection c) throws DatabaseException {
266         Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(c));
267         if (this.defaultSchema != null) {
268             database.setDefaultSchemaName(this.defaultSchema);
269         }
270         return database;
271     }
272 
273     public void setChangeLogParameters(Map<String, String> parameters) {
274         this.parameters = parameters;
275     }
276 
277     /**
278      * Create a new resourceOpener.
279      */
280     protected SpringResourceOpener createResourceOpener() {
281         return new SpringResourceOpener(getChangeLog());
282     }
283 
284     /**
285      * Spring sets this automatically to the instance's configured bean name.
286      */
287     public void setBeanName(String name) {
288         this.beanName = name;
289     }
290 
291     /**
292      * Gets the Spring-name of this instance.
293      * 
294      * @return
295      */
296     public String getBeanName() {
297         return beanName;
298     }
299 
300     public void setResourceLoader(ResourceLoader resourceLoader) {
301         this.resourceLoader = resourceLoader;
302     }
303 
304     public ResourceLoader getResourceLoader() {
305         return resourceLoader;
306     }
307 
308     @Override
309     public String toString() {
310         return getClass().getName() + "(" + this.getResourceLoader().toString() + ")";
311     }
312 }