001: package liquibase;
002:
003: import liquibase.change.Change;
004: import liquibase.database.Database;
005: import liquibase.database.sql.RawSqlStatement;
006: import liquibase.database.sql.SqlStatement;
007: import liquibase.exception.JDBCException;
008: import liquibase.exception.MigrationFailedException;
009: import liquibase.exception.RollbackFailedException;
010: import liquibase.exception.SetupException;
011: import liquibase.log.LogFactory;
012: import liquibase.util.MD5Util;
013: import liquibase.util.StreamUtil;
014: import liquibase.util.StringUtils;
015: import org.w3c.dom.Document;
016: import org.w3c.dom.Element;
017: import org.w3c.dom.Text;
018:
019: import java.io.IOException;
020: import java.io.Writer;
021: import java.util.*;
022: import java.util.logging.Logger;
023: import java.util.logging.Level;
024:
025: /**
026: * Encapsulates a changeSet and all its associated changes.
027: */
028: public class ChangeSet {
029:
030: public enum RunStatus {
031: NOT_RAN, ALREADY_RAN, RUN_AGAIN, INVALID_MD5SUM
032: }
033:
034: private List<Change> changes;
035: private String id;
036: private String author;
037: private String filePath = "UNKNOWN CHANGE LOG";
038: private String physicalFilePath;
039: private Logger log;
040: private String md5sum;
041: private boolean alwaysRun;
042: private boolean runOnChange;
043: private Set<String> contexts;
044: private Set<String> dbmsSet;
045: private Boolean failOnError;
046:
047: private SqlStatement[] rollBackStatements;
048:
049: private String comments;
050:
051: public boolean shouldAlwaysRun() {
052: return alwaysRun;
053: }
054:
055: public boolean shouldRunOnChange() {
056: return runOnChange;
057: }
058:
059: public ChangeSet(String id, String author, boolean alwaysRun,
060: boolean runOnChange, String filePath,
061: String physicalFilePath, String contextList, String dbmsList) {
062: this .changes = new ArrayList<Change>();
063: log = LogFactory.getLogger();
064: this .id = id;
065: this .author = author;
066: this .filePath = filePath;
067: this .physicalFilePath = physicalFilePath;
068: this .alwaysRun = alwaysRun;
069: this .runOnChange = runOnChange;
070: if (StringUtils.trimToNull(contextList) != null) {
071: String[] strings = contextList.toLowerCase().split(",");
072: contexts = new HashSet<String>();
073: for (String string : strings) {
074: contexts.add(string.trim().toLowerCase());
075: }
076: }
077: if (StringUtils.trimToNull(dbmsList) != null) {
078: String[] strings = dbmsList.toLowerCase().split(",");
079: dbmsSet = new HashSet<String>();
080: for (String string : strings) {
081: dbmsSet.add(string.trim().toLowerCase());
082: }
083: }
084: }
085:
086: public String getFilePath() {
087: return filePath;
088: }
089:
090: public String getPhysicalFilePath() {
091: if (physicalFilePath == null) {
092: return filePath;
093: } else {
094: return physicalFilePath;
095: }
096: }
097:
098: public String getMd5sum() {
099: if (md5sum == null) {
100: StringBuffer stringToMD5 = new StringBuffer();
101: for (Change change : getChanges()) {
102: stringToMD5.append(change.getMD5Sum()).append(":");
103: }
104:
105: md5sum = MD5Util.computeMD5(stringToMD5.toString());
106: }
107: return md5sum;
108: }
109:
110: /**
111: * This method will actually execute each of the changes in the list against the
112: * specified database.
113: */
114:
115: public void execute(Database database)
116: throws MigrationFailedException {
117:
118: try {
119: database.getJdbcTemplate().comment(
120: "Changeset " + toString());
121: if (StringUtils.trimToNull(getComments()) != null) {
122: String comments = getComments();
123: String[] lines = comments.split("\n");
124: for (int i = 0; i < lines.length; i++) {
125: if (i > 0) {
126: lines[i] = database.getLineComment() + " "
127: + lines[i];
128: }
129: }
130: database.getJdbcTemplate().comment(
131: StringUtils.join(Arrays.asList(lines), "\n"));
132: }
133:
134: for (Change change : changes) {
135: try {
136: change.setUp();
137: } catch (SetupException se) {
138: throw new MigrationFailedException(this , se);
139: }
140: }
141:
142: log.finest("Reading ChangeSet: " + toString());
143: for (Change change : getChanges()) {
144: change.executeStatements(database);
145: log.finest(change.getConfirmationMessage());
146: }
147:
148: database.commit();
149: log.finest("ChangeSet " + toString()
150: + " has been successfully ran.");
151:
152: database.commit();
153: } catch (Exception e) {
154: try {
155: database.rollback();
156: } catch (Exception e1) {
157: throw new MigrationFailedException(this , e);
158: }
159: if (getFailOnError() != null && !getFailOnError()) {
160: log.log(Level.INFO, "Change set " + toString(false)
161: + " failed, but failOnError was false", e);
162: } else {
163: throw new MigrationFailedException(this , e);
164: }
165: }
166: }
167:
168: public void rolback(Database database)
169: throws RollbackFailedException {
170: try {
171: database.getJdbcTemplate().comment(
172: "Rolling Back ChangeSet: " + toString());
173: if (rollBackStatements != null
174: && rollBackStatements.length > 0) {
175: for (SqlStatement rollback : rollBackStatements) {
176: try {
177: database.getJdbcTemplate().execute(rollback);
178: } catch (JDBCException e) {
179: throw new RollbackFailedException(
180: "Error executing custom SQL ["
181: + rollback
182: .getSqlStatement(database)
183: + "]", e);
184: }
185: }
186:
187: } else {
188: List<Change> changes = getChanges();
189: for (int i = changes.size() - 1; i >= 0; i--) {
190: Change change = changes.get(i);
191: change.executeRollbackStatements(database);
192: log.finest(change.getConfirmationMessage());
193: }
194: }
195:
196: database.commit();
197: log.finest("ChangeSet " + toString()
198: + " has been successfully rolled back.");
199: } catch (Exception e) {
200: throw new RollbackFailedException(e);
201: }
202:
203: }
204:
205: /**
206: * Returns an unmodifiable list of changes. To add one, use the addRefactoing method.
207: */
208: public List<Change> getChanges() {
209: return Collections.unmodifiableList(changes);
210: }
211:
212: public void addChange(Change change) {
213: changes.add(change);
214: }
215:
216: public String getId() {
217: return id;
218: }
219:
220: public String getAuthor() {
221: return author;
222: }
223:
224: public Set<String> getContexts() {
225: return contexts;
226: }
227:
228: public Set<String> getDbmsSet() {
229: return dbmsSet;
230: }
231:
232: public String toString(boolean includeMD5Sum) {
233: return filePath
234: + " :: "
235: + getId()
236: + " :: "
237: + getAuthor()
238: + (includeMD5Sum ? (" :: (MD5Sum: " + getMd5sum() + ")")
239: : "");
240: }
241:
242: public String toString() {
243: return toString(true);
244: }
245:
246: public String getComments() {
247: return comments;
248: }
249:
250: public void setComments(String comments) {
251: this .comments = comments;
252: }
253:
254: public Element createNode(Document currentChangeLogDOM) {
255: Element node = currentChangeLogDOM.createElement("changeSet");
256: node.setAttribute("id", getId());
257: node.setAttribute("author", getAuthor());
258:
259: if (alwaysRun) {
260: node.setAttribute("alwaysRun", "true");
261: }
262:
263: if (runOnChange) {
264: node.setAttribute("runOnChange", "true");
265: }
266:
267: if (failOnError != null) {
268: node.setAttribute("failOnError", failOnError.toString());
269: }
270:
271: if (getContexts() != null && getContexts().size() > 0) {
272: StringBuffer contextString = new StringBuffer();
273: for (String context : getContexts()) {
274: contextString.append(context).append(",");
275: }
276: node.setAttribute("context", contextString.toString()
277: .replaceFirst(",$", ""));
278: }
279:
280: if (getDbmsSet() != null && getDbmsSet().size() > 0) {
281: StringBuffer dbmsString = new StringBuffer();
282: for (String dbms : getDbmsSet()) {
283: dbmsString.append(dbms).append(",");
284: }
285: node.setAttribute("dbms", dbmsString.toString()
286: .replaceFirst(",$", ""));
287: }
288:
289: if (StringUtils.trimToNull(getComments()) != null) {
290: Element commentsElement = currentChangeLogDOM
291: .createElement("comment");
292: Text commentsText = currentChangeLogDOM
293: .createTextNode(getComments());
294: commentsElement.appendChild(commentsText);
295: node.appendChild(commentsElement);
296: }
297:
298: for (Change change : getChanges()) {
299: node.appendChild(change.createNode(currentChangeLogDOM));
300: }
301: return node;
302: }
303:
304: public SqlStatement[] getRollBackStatements() {
305: return rollBackStatements;
306: }
307:
308: public void setRollBackSQL(String sql) {
309: if (sql == null) {
310: return;
311: }
312: String[] sqlStatements = sql.split(";");
313: this .rollBackStatements = new SqlStatement[sqlStatements.length];
314:
315: for (int i = 0; i < rollBackStatements.length; i++) {
316: rollBackStatements[i] = new RawSqlStatement(
317: sqlStatements[i].trim());
318: }
319: }
320:
321: public boolean canRollBack() {
322: if (rollBackStatements != null && rollBackStatements.length > 0) {
323: return true;
324: }
325:
326: for (Change change : getChanges()) {
327: if (!change.canRollBack()) {
328: return false;
329: }
330: }
331: return true;
332: }
333:
334: public String getDescription() {
335: List<Change> changes = getChanges();
336: if (changes.size() == 0) {
337: return "Empty";
338: }
339:
340: StringBuffer returnString = new StringBuffer();
341: Class<? extends Change> lastChangeClass = null;
342: int changeCount = 0;
343: for (Change change : changes) {
344: if (change.getClass().equals(lastChangeClass)) {
345: changeCount++;
346: } else if (changeCount > 1) {
347: returnString.append(" (x").append(changeCount).append(
348: ")");
349: returnString.append(", ");
350: returnString.append(change.getChangeName());
351: changeCount = 1;
352: } else {
353: returnString.append(", ")
354: .append(change.getChangeName());
355: changeCount = 1;
356: }
357: lastChangeClass = change.getClass();
358: }
359:
360: if (changeCount > 1) {
361: returnString.append(" (x").append(changeCount).append(")");
362: }
363:
364: return returnString.toString().replaceFirst("^, ", "");
365: }
366:
367: public Boolean getFailOnError() {
368: return failOnError;
369: }
370:
371: public void setFailOnError(Boolean failOnError) {
372: this.failOnError = failOnError;
373: }
374: }
|