001: package liquibase.change;
002:
003: import liquibase.ChangeSet;
004: import liquibase.FileOpener;
005: import liquibase.database.Database;
006: import liquibase.database.sql.SqlStatement;
007: import liquibase.exception.*;
008: import liquibase.log.LogFactory;
009: import liquibase.util.MD5Util;
010: import liquibase.util.StreamUtil;
011: import liquibase.util.StringUtils;
012: import liquibase.util.XMLUtil;
013: import org.w3c.dom.Element;
014: import org.w3c.dom.NamedNodeMap;
015: import org.w3c.dom.Node;
016: import org.w3c.dom.NodeList;
017:
018: import javax.xml.parsers.DocumentBuilderFactory;
019: import javax.xml.parsers.ParserConfigurationException;
020: import java.io.IOException;
021: import java.io.Writer;
022: import java.util.*;
023:
024: /**
025: * Standard superclass for Changes to implement. This is a <i>skeletal implementation</i>,
026: * as defined in Effective Java#16.
027: *
028: * @see Change
029: */
030: public abstract class AbstractChange implements Change {
031:
032: /*
033: * The name and the tag name of the change.
034: * Defined as private members, so they can
035: * only be accessed through accessor methods
036: * by its subclasses
037: */
038: private final String changeName;
039: private final String tagName;
040: private FileOpener fileOpener;
041:
042: private ChangeSet changeSet;
043:
044: /**
045: * Constructor with tag name and name
046: *
047: * @param tagName the tag name for this change
048: * @param changeName the name for this change
049: */
050: protected AbstractChange(String tagName, String changeName) {
051: this .tagName = tagName;
052: this .changeName = changeName;
053: }
054:
055: //~ ------------------------------------------------------------------------------- public interface
056:
057: public ChangeSet getChangeSet() {
058: return changeSet;
059: }
060:
061: public void setChangeSet(ChangeSet changeSet) {
062: this .changeSet = changeSet;
063: }
064:
065: /**
066: * @see liquibase.change.Change#getChangeName()
067: */
068: public String getChangeName() {
069: return changeName;
070: }
071:
072: /**
073: * @see liquibase.change.Change#getTagName()
074: */
075: public String getTagName() {
076: return tagName;
077: }
078:
079: /**
080: * @see liquibase.change.Change#executeStatements(liquibase.database.Database)
081: */
082: public void executeStatements(Database database)
083: throws JDBCException, UnsupportedChangeException {
084: SqlStatement[] statements = generateStatements(database);
085:
086: execute(statements, database);
087: }
088:
089: /**
090: * @see liquibase.change.Change#saveStatements(liquibase.database.Database, java.io.Writer)
091: */
092: public void saveStatements(Database database, Writer writer)
093: throws IOException, UnsupportedChangeException,
094: StatementNotSupportedOnDatabaseException {
095: SqlStatement[] statements = generateStatements(database);
096: for (SqlStatement statement : statements) {
097: writer.append(statement.getSqlStatement(database)).append(
098: statement.getEndDelimiter(database)).append(
099: StreamUtil.getLineSeparator()).append(
100: StreamUtil.getLineSeparator());
101: }
102: }
103:
104: /**
105: * @see liquibase.change.Change#executeRollbackStatements(liquibase.database.Database)
106: */
107: public void executeRollbackStatements(Database database)
108: throws JDBCException, UnsupportedChangeException,
109: RollbackImpossibleException {
110: SqlStatement[] statements = generateRollbackStatements(database);
111: execute(statements, database);
112: }
113:
114: /**
115: * @see liquibase.change.Change#saveRollbackStatement(liquibase.database.Database, java.io.Writer)
116: */
117: public void saveRollbackStatement(Database database, Writer writer)
118: throws IOException, UnsupportedChangeException,
119: RollbackImpossibleException,
120: StatementNotSupportedOnDatabaseException {
121: SqlStatement[] statements = generateRollbackStatements(database);
122: for (SqlStatement statement : statements) {
123: writer.append(statement.getSqlStatement(database)).append(
124: ";\n\n");
125: }
126: }
127:
128: /*
129: * Skipped by this skeletal implementation
130: *
131: * @see liquibase.change.Change#generateStatements(liquibase.database.Database)
132: */
133:
134: /**
135: * @see liquibase.change.Change#generateRollbackStatements(liquibase.database.Database)
136: */
137: public SqlStatement[] generateRollbackStatements(Database database)
138: throws UnsupportedChangeException,
139: RollbackImpossibleException {
140: return generateRollbackStatementsFromInverse(database);
141: }
142:
143: /**
144: * @see liquibase.change.Change#canRollBack()
145: */
146: public boolean canRollBack() {
147: return createInverses() != null;
148: }
149:
150: /*
151: * Skipped by this skeletal implementation
152: *
153: * @see liquibase.change.Change#getConfirmationMessage()
154: */
155:
156: /*
157: * Skipped by this skeletal implementation
158: *
159: * @see liquibase.change.Change#createNode(org.w3c.dom.Document)
160: */
161:
162: /**
163: * @see liquibase.change.Change#getMD5Sum()
164: */
165: public String getMD5Sum() {
166: try {
167: StringBuffer buffer = new StringBuffer();
168: nodeToStringBuffer(createNode(DocumentBuilderFactory
169: .newInstance().newDocumentBuilder().newDocument()),
170: buffer);
171: return MD5Util.computeMD5(buffer.toString());
172: } catch (ParserConfigurationException e) {
173: throw new RuntimeException(e);
174: }
175: }
176:
177: //~ ------------------------------------------------------------------------------- private methods
178: /*
179: * Generates rollback statements from the inverse changes returned by createInverses()
180: *
181: * @param database the target {@link Database} associated to this change's rollback statements
182: * @return an array of {@link String}s containing the rollback statements from the inverse changes
183: * @throws UnsupportedChangeException if this change is not supported by the {@link Database} passed as argument
184: * @throws RollbackImpossibleException if rollback is not supported for this change
185: */
186: private SqlStatement[] generateRollbackStatementsFromInverse(
187: Database database) throws UnsupportedChangeException,
188: RollbackImpossibleException {
189: Change[] inverses = createInverses();
190: if (inverses == null) {
191: throw new RollbackImpossibleException("No inverse to "
192: + getClass().getName() + " created");
193: }
194:
195: List<SqlStatement> statements = new ArrayList<SqlStatement>();
196:
197: for (Change inverse : inverses) {
198: statements.addAll(Arrays.asList(inverse
199: .generateStatements(database)));
200: }
201:
202: return statements.toArray(new SqlStatement[statements.size()]);
203: }
204:
205: /*
206: * Create inverse changes that can roll back this change. This method is intended
207: * to be overriden by the subclasses that can create inverses.
208: *
209: * @return an array of {@link Change}s containing the inverse
210: * changes that can roll back this change
211: */
212: protected Change[] createInverses() {
213: return null;
214: }
215:
216: /*
217: * Creates a {@link String} using the XML element representation of this
218: * change
219: *
220: * @param node the {@link Element} associated to this change
221: * @param buffer a {@link StringBuffer} object used to hold the {@link String}
222: * representation of the change
223: */
224: private void nodeToStringBuffer(Element node, StringBuffer buffer) {
225: buffer.append("<").append(node.getNodeName());
226: SortedMap<String, String> attributeMap = new TreeMap<String, String>();
227: NamedNodeMap attributes = node.getAttributes();
228: for (int i = 0; i < attributes.getLength(); i++) {
229: Node attribute = attributes.item(i);
230: attributeMap.put(attribute.getNodeName(), attribute
231: .getNodeValue());
232: }
233: for (Map.Entry entry : attributeMap.entrySet()) {
234: String value = (String) entry.getValue();
235: if (value != null) {
236: buffer.append(" ").append(entry.getKey()).append("=\"")
237: .append(attributeMap.get(value)).append("\"");
238: }
239: }
240: buffer.append(">").append(
241: StringUtils.trimToEmpty(XMLUtil.getTextContent(node)));
242: NodeList childNodes = node.getChildNodes();
243: for (int i = 0; i < childNodes.getLength(); i++) {
244: Node childNode = childNodes.item(i);
245: if (childNode instanceof Element) {
246: nodeToStringBuffer(((Element) childNode), buffer);
247: }
248: }
249: buffer.append("</").append(node.getNodeName()).append(">");
250: }
251:
252: /*
253: * Executes the statements passed as argument to a target {@link Database}
254: *
255: * @param statements an array containing the SQL statements to be issued
256: * @param database the target {@link Database}
257: * @throws JDBCException if there were problems issuing the statements
258: */
259: private void execute(SqlStatement[] statements, Database database)
260: throws JDBCException {
261: for (SqlStatement statement : statements) {
262: LogFactory.getLogger().finest(
263: "Executing Statement: " + statement);
264: database.getJdbcTemplate().execute(statement);
265: }
266: }
267:
268: /**
269: * Default implementation that stores the file opener provided when the
270: * Change was created.
271: */
272: public void setFileOpener(FileOpener fileOpener) {
273: this .fileOpener = fileOpener;
274: }
275:
276: /**
277: * Returns the FileOpen as provided by the creating ChangeLog.
278: *
279: * @return The file opener
280: */
281: public FileOpener getFileOpener() {
282: return fileOpener;
283: }
284:
285: /**
286: * Most Changes don't need to do any setup.
287: * This implements a no-op
288: */
289: public void setUp() throws SetupException {
290:
291: }
292: }
|