001: /*
002: * Copyright 2006-2007, Unitils.org
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.unitils.dbmaintainer.version.impl;
017:
018: import static org.unitils.database.SQLUnitils.isEmpty;
019: import static org.unitils.thirdparty.org.apache.commons.dbutils.DbUtils.closeQuietly;
020:
021: import java.sql.Connection;
022: import java.sql.ResultSet;
023: import java.sql.SQLException;
024: import java.sql.Statement;
025: import java.util.Properties;
026: import java.util.Set;
027:
028: import org.apache.commons.logging.Log;
029: import org.apache.commons.logging.LogFactory;
030: import org.unitils.core.UnitilsException;
031: import org.unitils.dbmaintainer.util.BaseDatabaseTask;
032: import org.unitils.dbmaintainer.version.Version;
033: import org.unitils.dbmaintainer.version.VersionSource;
034: import org.unitils.util.PropertyUtils;
035:
036: /**
037: * Implementation of <code>VersionSource</code> that stores the version in the database. The version is stored in the
038: * table whose name is defined by the property {@link #PROPKEY_VERSION_TABLE_NAME}. The version index column name is
039: * defined by {@link #PROPKEY_VERSION_INDEX_COLUMN_NAME}, the version timestamp colmumn name is defined by
040: * {@link #PROPKEY_VERSION_TIMESTAMP_COLUMN_NAME}. The last updated succeeded column name is defined by
041: * {@link #PROPKEY_LAST_UPDATE_SUCCEEDED_COLUMN_NAME}.
042: *
043: * @author Filip Neven
044: * @author Tim Ducheyne
045: */
046: public class DBVersionSource extends BaseDatabaseTask implements
047: VersionSource {
048:
049: /* The logger instance for this class */
050: private static Log logger = LogFactory
051: .getLog(DBVersionSource.class);
052:
053: /* The key of the property that specifies the datase table in which the DB version is stored */
054: public static final String PROPKEY_VERSION_TABLE_NAME = "dbMaintainer.dbVersionSource.tableName";
055:
056: /* The key of the property that specifies the column in which the DB version index is stored */
057: public static final String PROPKEY_VERSION_INDEX_COLUMN_NAME = "dbMaintainer.dbVersionSource.versionIndexColumnName";
058:
059: /* The key of the property that specifies the column in which the DB version index is stored */
060: public static final String PROPKEY_VERSION_TIMESTAMP_COLUMN_NAME = "dbMaintainer.dbVersionSource.versionTimeStampColumnName";
061:
062: /* The key of the property that specifies the column in which is stored whether the last update succeeded. */
063: public static final String PROPKEY_LAST_UPDATE_SUCCEEDED_COLUMN_NAME = "dbMaintainer.dbVersionSource.lastUpdateSucceededColumnName";
064:
065: /* The key of the property that specifies the column in which the DB version index is stored */
066: public static final String PROPKEY_CODESCRIPTS_TIMESTAMP_COLUMN_NAME = "dbMaintainer.dbVersionSource.codeScriptsTimeStampColumnName";
067:
068: /* The key of the property that specifies the column in which is stored whether the last update succeeded. */
069: public static final String PROPKEY_LAST_CODE_UPDATE_SUCCEEDED_COLUMN_NAME = "dbMaintainer.dbVersionSource.lastCodeUpdateSucceededColumnName";
070:
071: /* The key of the property that specifies whether the version table should be created automatically. */
072: public static final String PROPKEY_AUTO_CREATE_VERSION_TABLE = "dbMaintainer.dbVersionSource.autoCreateVersionTable";
073:
074: /** The name of the datase table in which the DB version is stored */
075: protected String versionTableName;
076:
077: /** The name of the datase column in which the DB version index is stored */
078: protected String versionIndexColumnName;
079:
080: /** The name of the datase column in which the DB version timestamp is stored */
081: protected String versionTimestampColumnName;
082:
083: /** The name of the database column in which is stored whether the last DB update succeeded */
084: protected String lastUpdateSucceededColumnName;
085:
086: /** The name of the database column in which the DB code scripts timestamp is stored */
087: protected String codeScriptsTimestampColumnName;
088:
089: /** The name of the database column in which is stored whether the last code update succeeded */
090: protected String lastCodeUpdateSucceededColumnName;
091:
092: /** True if the version table should be created automatically if it does not exist yet */
093: protected boolean autoCreateVersionTable;
094:
095: /**
096: * Initializes the name of the version table and its columns using the given configuration.
097: *
098: * @param configuration the configuration, not null
099: */
100: @Override
101: protected void doInit(Properties configuration) {
102: this .versionTableName = PropertyUtils.getString(
103: PROPKEY_VERSION_TABLE_NAME, configuration);
104: this .versionIndexColumnName = PropertyUtils.getString(
105: PROPKEY_VERSION_INDEX_COLUMN_NAME, configuration);
106: this .versionTimestampColumnName = PropertyUtils.getString(
107: PROPKEY_VERSION_TIMESTAMP_COLUMN_NAME, configuration);
108: this .lastUpdateSucceededColumnName = PropertyUtils.getString(
109: PROPKEY_LAST_UPDATE_SUCCEEDED_COLUMN_NAME,
110: configuration);
111: this .codeScriptsTimestampColumnName = PropertyUtils.getString(
112: PROPKEY_CODESCRIPTS_TIMESTAMP_COLUMN_NAME,
113: configuration);
114: this .lastCodeUpdateSucceededColumnName = PropertyUtils
115: .getString(
116: PROPKEY_LAST_CODE_UPDATE_SUCCEEDED_COLUMN_NAME,
117: configuration);
118: this .autoCreateVersionTable = PropertyUtils.getBoolean(
119: PROPKEY_AUTO_CREATE_VERSION_TABLE, configuration);
120:
121: // convert to correct case
122: versionTableName = defaultDbSupport
123: .toCorrectCaseIdentifier(versionTableName);
124: versionIndexColumnName = defaultDbSupport
125: .toCorrectCaseIdentifier(versionIndexColumnName);
126: versionTimestampColumnName = defaultDbSupport
127: .toCorrectCaseIdentifier(versionTimestampColumnName);
128: lastUpdateSucceededColumnName = defaultDbSupport
129: .toCorrectCaseIdentifier(lastUpdateSucceededColumnName);
130: codeScriptsTimestampColumnName = defaultDbSupport
131: .toCorrectCaseIdentifier(codeScriptsTimestampColumnName);
132: lastCodeUpdateSucceededColumnName = defaultDbSupport
133: .toCorrectCaseIdentifier(lastCodeUpdateSucceededColumnName);
134: }
135:
136: /**
137: * Gets the current version from the version table in the database. The version table will be created (or altered)
138: * if needed.
139: *
140: * @return The current version of the database, not null
141: */
142: public Version getDbVersion() {
143: try {
144: return getDbVersionImpl();
145:
146: } catch (UnitilsException e) {
147: if (checkVersionTable()) {
148: throw e;
149: }
150: // try again, version table was not ok
151: return getDbVersionImpl();
152: }
153: }
154:
155: /**
156: * Updates the version of the database to the given value. The version table will be created (or altered) if needed.
157: *
158: * @param version The new version that the database should be updated to, not null
159: */
160: public void setDbVersion(Version version) {
161: try {
162: setDbVersionImpl(version);
163:
164: } catch (UnitilsException e) {
165: if (checkVersionTable()) {
166: throw e;
167: }
168: // try again, version table was not ok
169: setDbVersionImpl(version);
170: }
171: }
172:
173: /**
174: * Tells us whether the last database version update succeeded or not. The version table will be created (or
175: * altered) if needed.
176: *
177: * @return true if the last database version update succeeded, false otherwise
178: */
179: public boolean isLastUpdateSucceeded() {
180: try {
181: return isLastUpdateSucceededImpl();
182:
183: } catch (UnitilsException e) {
184: if (checkVersionTable()) {
185: throw e;
186: }
187: // try again, version table was not ok
188: return isLastUpdateSucceededImpl();
189: }
190: }
191:
192: /**
193: * Notifies the VersionSource of the fact that the lastest version update has succeeded or not. The version table
194: * will be created (or altered) if needed.
195: */
196: public void registerUpdateSucceeded(boolean succeeded) {
197: try {
198: registerUpdateSucceededImpl(succeeded);
199:
200: } catch (UnitilsException e) {
201: if (checkVersionTable()) {
202: throw e;
203: }
204: // try again, version table was not ok
205: registerUpdateSucceededImpl(succeeded);
206: }
207: }
208:
209: /**
210: * Tells us whether the last database code update succeeded or not
211: *
212: * @return true if the last database code update succeeded, false otherwise
213: */
214: public boolean isLastCodeUpdateSucceeded() {
215: try {
216: return isLastCodeUpdateSucceededImpl();
217:
218: } catch (UnitilsException e) {
219: if (checkVersionTable()) {
220: throw e;
221: }
222: // try again, version table was not ok
223: return isLastCodeUpdateSucceededImpl();
224: }
225: }
226:
227: /**
228: * Notifies the VersionSource of the fact that the lastest code update has succeeded or not
229: *
230: * @param succeeded True for success
231: */
232: public void registerCodeUpdateSucceeded(boolean succeeded) {
233: try {
234: registerCodeUpdateSucceededImpl(succeeded);
235:
236: } catch (UnitilsException e) {
237: if (checkVersionTable()) {
238: throw e;
239: }
240: // try again, version table was not ok
241: registerCodeUpdateSucceededImpl(succeeded);
242: }
243: }
244:
245: /**
246: * @return The current timestamp of the code scripts
247: */
248: public long getCodeScriptsTimestamp() {
249: try {
250: return getCodeScriptsTimestampImpl();
251:
252: } catch (UnitilsException e) {
253: if (checkVersionTable()) {
254: throw e;
255: }
256: // try again, version table was not ok
257: return getCodeScriptsTimestampImpl();
258: }
259: }
260:
261: /**
262: * Stores the timestamp of the code scripts in the VersionSource
263: *
264: * @param codeScriptsTimestamp The timestamp, not null
265: */
266: public void setCodeScriptsTimestamp(long codeScriptsTimestamp) {
267: try {
268: setCodeScriptsTimestampImpl(codeScriptsTimestamp);
269:
270: } catch (UnitilsException e) {
271: if (checkVersionTable()) {
272: throw e;
273: }
274: // try again, version table was not ok
275: setCodeScriptsTimestampImpl(codeScriptsTimestamp);
276: }
277: }
278:
279: /**
280: * @return The current version of the database
281: */
282: protected Version getDbVersionImpl() {
283: Connection conn = null;
284: Statement st = null;
285: ResultSet rs = null;
286: try {
287: conn = sqlHandler.getDataSource().getConnection();
288: st = conn.createStatement();
289: rs = st.executeQuery("select " + versionIndexColumnName
290: + ", " + versionTimestampColumnName + " from "
291: + defaultDbSupport.qualified(versionTableName));
292: rs.next();
293: return new Version(rs.getLong(versionIndexColumnName), rs
294: .getLong(versionTimestampColumnName));
295:
296: } catch (SQLException e) {
297: throw new UnitilsException(
298: "Error while retrieving database version", e);
299: } finally {
300: closeQuietly(conn, st, rs);
301: }
302: }
303:
304: /**
305: * Updates the version of the database to the given value
306: *
307: * @param version The new version that the database should be updated to
308: */
309: protected void setDbVersionImpl(Version version) {
310: int updateCount = sqlHandler.executeUpdate("update "
311: + defaultDbSupport.qualified(versionTableName)
312: + " set " + versionIndexColumnName + " = "
313: + version.getIndex() + ", "
314: + versionTimestampColumnName + " = "
315: + version.getTimeStamp() + ", "
316: + lastUpdateSucceededColumnName + " = 1");
317:
318: if (updateCount != 1 && sqlHandler.isDoExecuteUpdates()) {
319: throw new UnitilsException(
320: "Error while setting database version. There should be exactly 1 version record, found "
321: + updateCount);
322: }
323: }
324:
325: /**
326: * Tells us whether the last database version update succeeded or not
327: *
328: * @return True if the last database version update succeeded, false otherwise
329: */
330: protected boolean isLastUpdateSucceededImpl() {
331: Connection conn = null;
332: Statement st = null;
333: ResultSet rs = null;
334: try {
335: conn = sqlHandler.getDataSource().getConnection();
336: st = conn.createStatement();
337: rs = st.executeQuery("select "
338: + lastUpdateSucceededColumnName + " from "
339: + defaultDbSupport.qualified(versionTableName));
340: if (rs.next()) {
341: return (rs.getInt(lastUpdateSucceededColumnName) == 1);
342: } else {
343: return false;
344: }
345: } catch (SQLException e) {
346: throw new UnitilsException(
347: "Error while checking whether last update succeeded",
348: e);
349: } finally {
350: closeQuietly(conn, st, rs);
351: }
352: }
353:
354: /**
355: * Notifies the VersionSource of the fact that the lastest version update has succeeded or not
356: *
357: * @param succeeded True for success
358: */
359: protected void registerUpdateSucceededImpl(boolean succeeded) {
360: int updateCount = sqlHandler.executeUpdate("update "
361: + defaultDbSupport.qualified(versionTableName)
362: + " set " + lastUpdateSucceededColumnName + " = "
363: + (succeeded ? "1" : "0"));
364: if (updateCount != 1 && sqlHandler.isDoExecuteUpdates()) {
365: throw new UnitilsException(
366: "Error while registering update succeeded. There should be exactly 1 version record, found "
367: + updateCount);
368: }
369: }
370:
371: /**
372: * @return The current version of the database
373: */
374: protected long getCodeScriptsTimestampImpl() {
375: Connection conn = null;
376: Statement st = null;
377: ResultSet rs = null;
378: try {
379: conn = sqlHandler.getDataSource().getConnection();
380: st = conn.createStatement();
381: rs = st.executeQuery("select "
382: + codeScriptsTimestampColumnName + " from "
383: + defaultDbSupport.qualified(versionTableName));
384: rs.next();
385: return rs.getLong(codeScriptsTimestampColumnName);
386:
387: } catch (SQLException e) {
388: throw new UnitilsException(
389: "Error while retrieving database version", e);
390: } finally {
391: closeQuietly(conn, st, rs);
392: }
393: }
394:
395: /**
396: * Updates the timestamp of the database code to the given value
397: *
398: * @param timestamp The new timestamp
399: */
400: protected void setCodeScriptsTimestampImpl(long timestamp) {
401: int updateCount = sqlHandler.executeCodeUpdate("update "
402: + defaultDbSupport.qualified(versionTableName)
403: + " set " + codeScriptsTimestampColumnName + " = "
404: + timestamp + ", " + lastCodeUpdateSucceededColumnName
405: + " = 1");
406:
407: if (updateCount != 1 && sqlHandler.isDoExecuteUpdates()) {
408: throw new UnitilsException(
409: "Error while setting database version. There should be exactly 1 version record, found "
410: + updateCount);
411: }
412: }
413:
414: /**
415: * Tells us whether the last database version update succeeded or not
416: *
417: * @return True if the last database version update succeeded, false otherwise
418: */
419: protected boolean isLastCodeUpdateSucceededImpl() {
420: Connection conn = null;
421: Statement st = null;
422: ResultSet rs = null;
423: try {
424: conn = sqlHandler.getDataSource().getConnection();
425: st = conn.createStatement();
426: rs = st.executeQuery("select "
427: + lastCodeUpdateSucceededColumnName + " from "
428: + defaultDbSupport.qualified(versionTableName));
429: if (rs.next()) {
430: return (rs.getInt(lastCodeUpdateSucceededColumnName) == 1);
431: } else {
432: return false;
433: }
434: } catch (SQLException e) {
435: throw new UnitilsException(
436: "Error while checking whether last update succeeded",
437: e);
438: } finally {
439: closeQuietly(conn, st, rs);
440: }
441: }
442:
443: /**
444: * Notifies the VersionSource of the fact that the code script update has succeeded or not
445: *
446: * @param succeeded True for success
447: */
448: protected void registerCodeUpdateSucceededImpl(boolean succeeded) {
449: int updateCount = sqlHandler.executeCodeUpdate("update "
450: + defaultDbSupport.qualified(versionTableName)
451: + " set " + lastCodeUpdateSucceededColumnName + " = "
452: + (succeeded ? "1" : "0"));
453:
454: if (updateCount != 1 && sqlHandler.isDoExecuteUpdates()) {
455: throw new UnitilsException(
456: "Error while registering update succeeded. There should be exactly 1 version record, found "
457: + updateCount);
458: }
459: }
460:
461: /**
462: * Checks if the version table and columns are available and if a record exists in which the version info is stored.
463: * If not, the table, columns and record are created if auto-create is true, else an exception is raised.
464: *
465: * @return False if the version table was not ok and therefore re-created
466: */
467: protected boolean checkVersionTable() {
468: // check valid
469: if (isVersionTableValid()) {
470: return checkVersionRecord();
471: }
472:
473: // does not exist yet, if auto-create create version table
474: if (autoCreateVersionTable) {
475: logger
476: .warn("Version table "
477: + defaultDbSupport
478: .qualified(versionTableName)
479: + " doesn't exist yet or is invalid. A new one is created automatically.");
480: createVersionTable();
481: return false;
482: }
483:
484: // throw an exception that shows how to create the version table
485: String message = "Version table "
486: + defaultDbSupport.qualified(versionTableName)
487: + " doesn't exist yet or is invalid.\n";
488: message += "Please create a version table manually or let Unitils create it automatically by setting the "
489: + PROPKEY_AUTO_CREATE_VERSION_TABLE
490: + " property to true.\n";
491: message += "The version table can be created manually by executing following statement:\n";
492: message += getCreateVersionTableStatement();
493: throw new UnitilsException(message);
494: }
495:
496: /**
497: * Checks whether the version table contains a record with version info. If not a record is inserted.
498: *
499: * @return False if the version record was not ok and therefore inserted.
500: */
501: protected boolean checkVersionRecord() {
502: // Check contains valid record
503: if (!isEmpty(defaultDbSupport.qualified(versionTableName),
504: sqlHandler.getDataSource())) {
505: return true;
506: }
507:
508: // Does not exist yet, insert a record with default version numbers.
509: sqlHandler.executeUpdate(getInsertVersionRecordStatement());
510: return false;
511: }
512:
513: /**
514: * Checks if the version table and columns are available and if a record exists in which the version info is stored.
515: * If not, the table, columns and record are created.
516: *
517: * @return False if the version table was not ok and therefore re-created
518: */
519: protected boolean isVersionTableValid() {
520: // Check existence of version table
521: Set<String> tableNames = defaultDbSupport.getTableNames();
522: if (tableNames.contains(versionTableName)) {
523: // Check columns of version table
524: Set<String> columnNames = defaultDbSupport
525: .getColumnNames(versionTableName);
526: if (columnNames.contains(versionIndexColumnName)
527: && columnNames.contains(versionTimestampColumnName)
528: && columnNames
529: .contains(lastUpdateSucceededColumnName)
530: && columnNames
531: .contains(codeScriptsTimestampColumnName)
532: && columnNames
533: .contains(lastCodeUpdateSucceededColumnName)) {
534: return true;
535: }
536: }
537: return false;
538: }
539:
540: /**
541: * Creates the version table and inserts a version record.
542: */
543: protected void createVersionTable() {
544: // If version table is invalid, drop and re-create
545: try {
546: defaultDbSupport.dropTable(versionTableName);
547: } catch (UnitilsException e) {
548: // ignored
549: }
550:
551: // Create db version table
552: sqlHandler.executeUpdate(getCreateVersionTableStatement());
553: // insert default version record
554: checkVersionRecord();
555: }
556:
557: /**
558: * @return The statment to create the version table.
559: */
560: protected String getCreateVersionTableStatement() {
561: String longDataType = defaultDbSupport.getLongDataType();
562: return "create table "
563: + defaultDbSupport.qualified(versionTableName) + " ( "
564: + versionIndexColumnName + " " + longDataType + ", "
565: + versionTimestampColumnName + " " + longDataType
566: + ", " + lastUpdateSucceededColumnName + " "
567: + longDataType + ", " + codeScriptsTimestampColumnName
568: + " " + longDataType + ", "
569: + lastCodeUpdateSucceededColumnName + " "
570: + longDataType + " )";
571: }
572:
573: /**
574: * @return The statment to insert the version record in the version table.
575: */
576: protected String getInsertVersionRecordStatement() {
577: return "insert into "
578: + defaultDbSupport.qualified(versionTableName) + " ("
579: + versionIndexColumnName + ", "
580: + versionTimestampColumnName + ", "
581: + lastUpdateSucceededColumnName + ", "
582: + codeScriptsTimestampColumnName + ", "
583: + lastCodeUpdateSucceededColumnName
584: + ") values (0, 0, 0, 0, 0)";
585: }
586: }
|