001: package liquibase.spring;
002:
003: import org.springframework.beans.factory.InitializingBean;
004: import org.springframework.beans.factory.BeanNameAware;
005: import org.springframework.context.ResourceLoaderAware;
006: import org.springframework.core.io.Resource;
007: import org.springframework.core.io.ResourceLoader;
008: import liquibase.FileOpener;
009: import liquibase.Liquibase;
010: import liquibase.database.Database;
011: import liquibase.database.DatabaseFactory;
012: import liquibase.exception.JDBCException;
013: import liquibase.exception.LiquibaseException;
014:
015: import javax.sql.DataSource;
016: import java.io.InputStream;
017: import java.io.IOException;
018: import java.io.File;
019: import java.io.FileWriter;
020: import java.net.URL;
021: import java.util.Enumeration;
022: import java.util.Vector;
023: import java.util.TimeZone;
024: import java.util.logging.Logger;
025: import java.util.logging.Level;
026: import java.sql.Connection;
027: import java.sql.SQLException;
028: import java.text.SimpleDateFormat;
029:
030: /**
031: * A Spring-ified wrapper for Liquibase.
032: *
033: * Example Configuration:
034: * <p>
035: * <p>
036: * This Spring configuration example will cause liquibase to run
037: * automatically when the Spring context is initialized. It will load
038: * <code>db-changelog.xml</code> from the classpath and apply it against
039: * <code>myDataSource</code>.
040: * <p>
041: *
042: * <pre>
043: * <bean id="myLiquibase"
044: * class="liquibase.spring.SpringLiquibase"
045: * >
046: *
047: * <property name="dataSource" ref="myDataSource" />
048: *
049: * <property name="changeLog" value="classpath:db-changelog.xml" />
050: *
051: * <!-- The following configuration options are optional -->
052: *
053: * <property name="executeEnabled" value="true" />
054: *
055: * <!--
056: * If set to true, writeSqlFileEnabled will write the generated
057: * SQL to a file before executing it.
058: * -->
059: * <property name="writeSqlFileEnabled" value="true" />
060: *
061: * <!--
062: * sqlOutputDir specifies the directory into which the SQL file
063: * will be written, if so configured.
064: * -->
065: * <property name="sqlOutputDir" value="c:\sql" />
066: *
067: * </bean>
068: *
069: * </pre>
070: *
071: * @author Rob Schoening
072: */
073: public class SpringLiquibase implements InitializingBean,
074: BeanNameAware, ResourceLoaderAware {
075: public class SpringResourceOpener implements FileOpener {
076: private String parentFile;
077:
078: public SpringResourceOpener(String parentFile) {
079: this .parentFile = parentFile;
080: }
081:
082: public InputStream getResourceAsStream(String file)
083: throws IOException {
084: Resource resource = getResource(file);
085:
086: return resource.getInputStream();
087: }
088:
089: public Enumeration<URL> getResources(String packageName)
090: throws IOException {
091: Vector<URL> tmp = new Vector<URL>();
092:
093: tmp.add(getResource(packageName).getURL());
094:
095: return tmp.elements();
096: }
097:
098: public Resource getResource(String file) {
099: return getResourceLoader().getResource(
100: adjustClasspath(file));
101: }
102:
103: private String adjustClasspath(String file) {
104: return isClasspathPrefixPresent(parentFile)
105: && !isClasspathPrefixPresent(file) ? ResourceLoader.CLASSPATH_URL_PREFIX
106: + file
107: : file;
108: }
109:
110: public boolean isClasspathPrefixPresent(String file) {
111: return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
112: }
113:
114: public ClassLoader toClassLoader() {
115: return getResourceLoader().getClassLoader();
116: }
117: }
118:
119: private String beanName;
120:
121: private ResourceLoader resourceLoader;
122:
123: private DataSource dataSource;
124:
125: private boolean executeEnabled = true;
126:
127: private boolean writeSqlFileEnabled = true;
128:
129: private Logger log = Logger.getLogger(SpringLiquibase.class
130: .getName());
131:
132: private String changeLog;
133:
134: private String contexts;
135:
136: private File sqlOutputDir;
137:
138: public SpringLiquibase() {
139: super ();
140: }
141:
142: public String getDatabaseProductName() throws JDBCException {
143: Connection connection = null;
144: String name = "unknown";
145: try {
146: connection = getDataSource().getConnection();
147: Database database = DatabaseFactory.getInstance()
148: .findCorrectDatabaseImplementation(
149: dataSource.getConnection());
150: name = database.getDatabaseProductName();
151: } catch (SQLException e) {
152: throw new JDBCException(e);
153: } finally {
154: if (connection != null) {
155: try {
156: connection.rollback();
157: connection.close();
158: } catch (Exception e) {
159: log.log(Level.WARNING,
160: "problem closing connection", e);
161: }
162: }
163: }
164: return name;
165: }
166:
167: /**
168: * The DataSource that liquibase will use to perform the migration.
169: *
170: * @return
171: */
172: public DataSource getDataSource() {
173: return dataSource;
174: }
175:
176: /**
177: * The DataSource that liquibase will use to perform the migration.
178: */
179: public void setDataSource(DataSource dataSource) {
180: this .dataSource = dataSource;
181: }
182:
183: /**
184: * Determines whether liquibase will actually execute DDL statements.
185: *
186: * @param executeSql
187: */
188: public void setExecuteEnabled(boolean executeSql) {
189: this .executeEnabled = executeSql;
190: }
191:
192: /**
193: * Determines whether liquibase will actually execute DDL statements.
194: */
195: public boolean isExecuteEnabled() {
196: return executeEnabled;
197: }
198:
199: /**
200: * Returns a warning message that will be written out to the log. It is here so that it
201: * can be customized.
202: *
203: * @return
204: */
205: public String getExecuteDisabledWarningMessage() {
206: return "\n\nYou have set "
207: + getBeanName()
208: + ".executeEnabled=false, but there are \n"
209: + "database change sets that need to be run in order to bring the database up to date.\n"
210: + "The application may not behave properly until these changes are run.\n\n";
211:
212: }
213:
214: /**
215: * Returns the output directory into which a SQL file will be written *if*
216: * writeSqlFileEnabled is set to true.
217: *
218: * @return
219: */
220: public File getSqlOutputDir() {
221: return sqlOutputDir;
222: }
223:
224: /**
225: * Sets the output directory into which a SQL file will be written if
226: * writeSqlFileEnabled is also set to true.
227: *
228: * @param sqlOutputDir
229: */
230: public void setSqlOutputDir(File sqlOutputDir) {
231: this .sqlOutputDir = sqlOutputDir;
232: }
233:
234: /**
235: * Returns a Resource that is able to resolve to a file or classpath resource.
236: *
237: * @return
238: */
239: public String getChangeLog() {
240: return changeLog;
241: }
242:
243: /**
244: * Sets a Spring Resource that is able to resolve to a file or classpath resource.
245: * An example might be <code>classpath:db-changelog.xml</code>.
246: */
247: public void setChangeLog(String dataModel) {
248:
249: this .changeLog = dataModel;
250: }
251:
252: public String getContexts() {
253: return contexts;
254: }
255:
256: public void setContexts(String contexts) {
257: this .contexts = contexts;
258: }
259:
260: /**
261: * Executed automatically when the bean is initialized.
262: */
263: public void afterPropertiesSet() throws LiquibaseException {
264: Connection c = null;
265: try {
266: c = getDataSource().getConnection();
267: Liquibase liquibase = createLiquibase(c);
268:
269: if (isWriteSqlFileEnabled() && getSqlOutputDir() != null) {
270: if (liquibase.listUnrunChangeSets(getContexts()).size() > 0) {
271: log.log(Level.WARNING,
272: getExecuteDisabledWarningMessage());
273: }
274: writeSqlFile(liquibase);
275: }
276:
277: // Now execute the DDL, if so configured
278: if (isExecuteEnabled()) {
279: executeSql(liquibase);
280: }
281: } catch (SQLException e) {
282: throw new JDBCException(e);
283: } finally {
284: if (c != null) {
285: try {
286: c.rollback();
287: c.close();
288: } catch (SQLException e) {
289: ;
290: }
291: }
292: }
293:
294: }
295:
296: /**
297: * If there are any un-run changesets, this method will write them out to a file, if
298: * so configured.
299: *
300: * @param liquibase
301: *
302: * @throws SQLException
303: * @throws IOException
304: * @throws LiquibaseException
305: */
306: protected void writeSqlFile(Liquibase liquibase)
307: throws LiquibaseException {
308: FileWriter fw = null;
309: try {
310:
311: File ddlDir = getSqlOutputDir();
312: ddlDir.mkdirs();
313: SimpleDateFormat sdf = new SimpleDateFormat(
314: "yyyyMMddHHmmss");
315: sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
316: String dateString = sdf.format(new java.util.Date());
317:
318: String fileString = (getDatabaseProductName() + "-"
319: + dateString + ".sql").replace(" ", "-")
320: .toLowerCase();
321: File upgradeFile = new File(ddlDir, fileString);
322: fw = new FileWriter(upgradeFile);
323:
324: liquibase.update(getContexts(), fw);
325: } catch (IOException e) {
326: throw new LiquibaseException(e);
327: } finally {
328: if (fw != null) {
329: try {
330: fw.close();
331: } catch (IOException e) {
332: log
333: .log(Level.SEVERE,
334: "error closing fileWriter", e);
335: }
336: }
337: }
338: }
339:
340: private Liquibase createLiquibase(Connection c)
341: throws JDBCException {
342: return new Liquibase(getChangeLog(), new SpringResourceOpener(
343: getChangeLog()), DatabaseFactory.getInstance()
344: .findCorrectDatabaseImplementation(c));
345: }
346:
347: /**
348: * This method will actually execute the changesets.
349: *
350: * @param liquibase
351: *
352: * @throws SQLException
353: * @throws IOException
354: */
355: protected void executeSql(Liquibase liquibase)
356: throws LiquibaseException {
357: liquibase.update(getContexts());
358: }
359:
360: /**
361: * Boolean flag to determine whether generated SQL should be written to disk.
362: *
363: * @return
364: */
365: public boolean isWriteSqlFileEnabled() {
366: return writeSqlFileEnabled;
367: }
368:
369: /**
370: * Boolean flag to determine whether generated SQL should be written to disk.
371: */
372: public void setWriteSqlFileEnabled(boolean writeSqlFile) {
373: this .writeSqlFileEnabled = writeSqlFile;
374: }
375:
376: /**
377: * Spring sets this automatically to the instance's configured bean name.
378: */
379: public void setBeanName(String name) {
380: this .beanName = name;
381: }
382:
383: /**
384: * Gets the Spring-name of this instance.
385: *
386: * @return
387: */
388: public String getBeanName() {
389: return beanName;
390: }
391:
392: public void setResourceLoader(ResourceLoader resourceLoader) {
393: this .resourceLoader = resourceLoader;
394: }
395:
396: public ResourceLoader getResourceLoader() {
397: return resourceLoader;
398: }
399: }
|