View Javadoc

1   package liquibase.parser.core.xml;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.lang.reflect.InvocationTargetException;
7   import java.lang.reflect.Method;
8   import java.net.URL;
9   import java.net.URLDecoder;
10  import java.util.Arrays;
11  import java.util.Comparator;
12  import java.util.Enumeration;
13  import java.util.HashSet;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.Properties;
17  import java.util.Set;
18  import java.util.SortedSet;
19  import java.util.Stack;
20  import java.util.TreeSet;
21  import java.util.jar.JarEntry;
22  import java.util.jar.JarFile;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import liquibase.change.Change;
27  import liquibase.change.ChangeFactory;
28  import liquibase.change.ChangeWithColumns;
29  import liquibase.change.ColumnConfig;
30  import liquibase.change.ConstraintsConfig;
31  import liquibase.change.core.CreateProcedureChange;
32  import liquibase.change.core.CreateViewChange;
33  import liquibase.change.core.DeleteDataChange;
34  import liquibase.change.core.ExecuteShellCommandChange;
35  import liquibase.change.core.InsertDataChange;
36  import liquibase.change.core.LoadDataChange;
37  import liquibase.change.core.LoadDataColumnConfig;
38  import liquibase.change.core.RawSQLChange;
39  import liquibase.change.core.StopChange;
40  import liquibase.change.core.UpdateDataChange;
41  import liquibase.change.custom.CustomChangeWrapper;
42  import liquibase.changelog.ChangeLogParameters;
43  import liquibase.changelog.ChangeSet;
44  import liquibase.changelog.DatabaseChangeLog;
45  import liquibase.exception.CustomChangeException;
46  import liquibase.exception.LiquibaseException;
47  import liquibase.exception.MigrationFailedException;
48  import liquibase.logging.LogFactory;
49  import liquibase.logging.Logger;
50  import liquibase.parser.ChangeLogParserFactory;
51  import liquibase.precondition.CustomPreconditionWrapper;
52  import liquibase.precondition.Precondition;
53  import liquibase.precondition.PreconditionFactory;
54  import liquibase.precondition.PreconditionLogic;
55  import liquibase.precondition.core.PreconditionContainer;
56  import liquibase.precondition.core.SqlPrecondition;
57  import liquibase.resource.ResourceAccessor;
58  import liquibase.sql.visitor.SqlVisitor;
59  import liquibase.sql.visitor.SqlVisitorFactory;
60  import liquibase.util.ObjectUtil;
61  import liquibase.util.StringUtils;
62  import liquibase.util.file.FilenameUtils;
63  
64  import org.xml.sax.Attributes;
65  import org.xml.sax.SAXException;
66  import org.xml.sax.helpers.DefaultHandler;
67  
68  class XMLChangeLogSAXHandler extends DefaultHandler {
69  
70      private static final char LIQUIBASE_FILE_SEPARATOR = '/';
71  
72      protected Logger log;
73  
74      private DatabaseChangeLog databaseChangeLog;
75      private Change change;
76      private Stack changeSubObjects = new Stack();
77      private StringBuffer text;
78      private PreconditionContainer rootPrecondition;
79      private Stack<PreconditionLogic> preconditionLogicStack = new Stack<PreconditionLogic>();
80      private ChangeSet changeSet;
81      private String paramName;
82      private ResourceAccessor resourceAccessor;
83      private Precondition currentPrecondition;
84  
85      private ChangeLogParameters changeLogParameters;
86      private boolean inRollback = false;
87  
88      private boolean inModifySql = false;
89      private Set<String> modifySqlDbmsList;
90      private Set<String> modifySqlContexts;
91      private boolean modifySqlAppliedOnRollback = false;
92  
93      protected XMLChangeLogSAXHandler(String physicalChangeLogLocation, ResourceAccessor resourceAccessor,
94              ChangeLogParameters changeLogParameters) {
95          log = LogFactory.getLogger();
96          this.resourceAccessor = resourceAccessor;
97  
98          databaseChangeLog = new DatabaseChangeLog();
99          databaseChangeLog.setPhysicalFilePath(physicalChangeLogLocation);
100         databaseChangeLog.setChangeLogParameters(changeLogParameters);
101 
102         this.changeLogParameters = changeLogParameters;
103     }
104 
105     public DatabaseChangeLog getDatabaseChangeLog() {
106         return databaseChangeLog;
107     }
108 
109     @Override
110     public void startElement(String uri, String localName, String qName, Attributes baseAttributes) throws SAXException {
111         Attributes atts = new ExpandingAttributes(baseAttributes);
112         try {
113             if ("comment".equals(qName)) {
114                 text = new StringBuffer();
115             } else if ("validCheckSum".equals(qName)) {
116                 text = new StringBuffer();
117             } else if ("databaseChangeLog".equals(qName)) {
118                 String schemaLocation = atts.getValue("xsi:schemaLocation");
119                 if (schemaLocation != null) {
120                     Matcher matcher = Pattern.compile(".*dbchangelog-(\\d+\\.\\d+).xsd").matcher(schemaLocation);
121                     if (matcher.matches()) {
122                         String version = matcher.group(1);
123                         if (!version.equals(XMLChangeLogSAXParser.getSchemaVersion())) {
124                             log.info(databaseChangeLog.getPhysicalFilePath() + " is using schema version " + version
125                                     + " rather than version " + XMLChangeLogSAXParser.getSchemaVersion());
126                         }
127                     }
128                 }
129                 databaseChangeLog.setLogicalFilePath(atts.getValue("logicalFilePath"));
130             } else if ("include".equals(qName)) {
131                 String fileName = atts.getValue("file");
132                 fileName = fileName.replace('\\', '/');
133                 boolean isRelativeToChangelogFile = Boolean.parseBoolean(atts.getValue("relativeToChangelogFile"));
134                 handleIncludedChangeLog(fileName, isRelativeToChangelogFile, databaseChangeLog.getPhysicalFilePath());
135             } else if ("includeAll".equals(qName)) {
136                 String pathName = atts.getValue("path");
137                 pathName = pathName.replace('\\', '/');
138 
139                 if (!(pathName.endsWith("/"))) {
140                     pathName = pathName + '/';
141                 }
142                 log.debug("includeAll for " + pathName);
143                 log.debug("Using file opener for includeAll: " + resourceAccessor.toString());
144                 boolean isRelativeToChangelogFile = Boolean.parseBoolean(atts.getValue("relativeToChangelogFile"));
145 
146                 if (isRelativeToChangelogFile) {
147                     File changeLogFile = new File(databaseChangeLog.getPhysicalFilePath());
148                     File resourceBase = new File(changeLogFile.getParent(), pathName);
149                     if (!resourceBase.exists()) {
150                         throw new SAXException("Resource directory for includeAll does not exist ["
151                                 + resourceBase.getPath() + "]");
152                     }
153                     pathName = resourceBase.getPath() + '/';
154                     pathName = pathName.replace('\\', '/');
155                 }
156 
157                 Enumeration<URL> resourcesEnum = resourceAccessor.getResources(pathName);
158                 SortedSet<URL> resources = new TreeSet<URL>(new Comparator<URL>() {
159                     @Override
160                     public int compare(URL o1, URL o2) {
161                         return o1.toString().compareTo(o2.toString());
162                     }
163                 });
164                 while (resourcesEnum.hasMoreElements()) {
165                     resources.add(resourcesEnum.nextElement());
166                 }
167 
168                 boolean foundResource = false;
169 
170                 Set<String> seenPaths = new HashSet<String>();
171                 for (URL fileUrl : resources) {
172                     if (!fileUrl.toExternalForm().startsWith("file:")) {
173                         if (fileUrl.toExternalForm().startsWith("jar:file:")
174                                 || fileUrl.toExternalForm().startsWith("wsjar:file:")
175                                 || fileUrl.toExternalForm().startsWith("zip:")) {
176                             File zipFileDir = extractZipFile(fileUrl);
177                             fileUrl = new File(zipFileDir, pathName).toURL();
178                         } else {
179                             log.debug(fileUrl.toExternalForm() + " is not a file path");
180                             continue;
181                         }
182                     }
183                     File file = new File(fileUrl.toURI());
184                     log.debug("includeAll using path " + file.getCanonicalPath());
185                     if (!file.exists()) {
186                         throw new SAXException("includeAll path " + pathName + " could not be found.  Tried in "
187                                 + file.toString());
188                     }
189                     if (file.isDirectory()) {
190                         log.debug(file.getCanonicalPath() + " is a directory");
191                         for (File childFile : new TreeSet<File>(Arrays.asList(file.listFiles()))) {
192                             String path = pathName + childFile.getName();
193                             if (!seenPaths.add(path)) {
194                                 log.debug("already included " + path);
195                                 continue;
196                             }
197 
198                             if (handleIncludedChangeLog(path, false, databaseChangeLog.getPhysicalFilePath())) {
199                                 foundResource = true;
200                             }
201                         }
202                     } else {
203                         String path = pathName + file.getName();
204                         if (!seenPaths.add(path)) {
205                             log.debug("already included " + path);
206                             continue;
207                         }
208                         if (handleIncludedChangeLog(path, false, databaseChangeLog.getPhysicalFilePath())) {
209                             foundResource = true;
210                         }
211                     }
212                 }
213 
214                 if (!foundResource) {
215                     throw new SAXException("Could not find directory or directory was empty for includeAll '"
216                             + pathName + "'");
217                 }
218             } else if (changeSet == null && "changeSet".equals(qName)) {
219                 boolean alwaysRun = false;
220                 boolean runOnChange = false;
221                 if ("true".equalsIgnoreCase(atts.getValue("runAlways"))) {
222                     alwaysRun = true;
223                 }
224                 if ("true".equalsIgnoreCase(atts.getValue("runOnChange"))) {
225                     runOnChange = true;
226                 }
227                 String filePath = atts.getValue("logicalFilePath");
228                 if (filePath == null || "".equals(filePath)) {
229                     filePath = databaseChangeLog.getFilePath();
230                 }
231 
232                 changeSet = new ChangeSet(atts.getValue("id"), atts.getValue("author"), alwaysRun, runOnChange,
233                         filePath, atts.getValue("context"), atts.getValue("dbms"), Boolean.valueOf(atts
234                                 .getValue("runInTransaction")));
235                 if (StringUtils.trimToNull(atts.getValue("failOnError")) != null) {
236                     changeSet.setFailOnError(Boolean.parseBoolean(atts.getValue("failOnError")));
237                 }
238                 if (StringUtils.trimToNull(atts.getValue("onValidationFail")) != null) {
239                     changeSet.setOnValidationFail(ChangeSet.ValidationFailOption.valueOf(atts
240                             .getValue("onValidationFail")));
241                 }
242             } else if (changeSet != null && "rollback".equals(qName)) {
243                 text = new StringBuffer();
244                 String id = atts.getValue("changeSetId");
245                 if (id != null) {
246                     String path = atts.getValue("changeSetPath");
247                     if (path == null) {
248                         path = databaseChangeLog.getFilePath();
249                     }
250                     String author = atts.getValue("changeSetAuthor");
251                     ChangeSet changeSet = databaseChangeLog.getChangeSet(path, author, id);
252                     if (changeSet == null) {
253                         throw new SAXException("Could not find changeSet to use for rollback: " + path + ":" + author
254                                 + ":" + id);
255                     } else {
256                         for (Change change : changeSet.getChanges()) {
257                             this.changeSet.addRollbackChange(change);
258                         }
259                     }
260                 }
261                 inRollback = true;
262             } else if ("preConditions".equals(qName)) {
263                 rootPrecondition = new PreconditionContainer();
264                 rootPrecondition.setOnFail(StringUtils.trimToNull(atts.getValue("onFail")));
265                 rootPrecondition.setOnError(StringUtils.trimToNull(atts.getValue("onError")));
266                 rootPrecondition.setOnFailMessage(StringUtils.trimToNull(atts.getValue("onFailMessage")));
267                 rootPrecondition.setOnErrorMessage(StringUtils.trimToNull(atts.getValue("onErrorMessage")));
268                 rootPrecondition.setOnSqlOutput(StringUtils.trimToNull(atts.getValue("onSqlOutput")));
269                 preconditionLogicStack.push(rootPrecondition);
270             } else if (currentPrecondition != null && currentPrecondition instanceof CustomPreconditionWrapper
271                     && qName.equals("param")) {
272                 ((CustomPreconditionWrapper) currentPrecondition).setParam(atts.getValue("name"),
273                         atts.getValue("value"));
274             } else if (rootPrecondition != null) {
275                 currentPrecondition = PreconditionFactory.getInstance().create(qName);
276 
277                 for (int i = 0; i < atts.getLength(); i++) {
278                     String attributeName = atts.getQName(i);
279                     String attributeValue = atts.getValue(i);
280                     setProperty(currentPrecondition, attributeName, attributeValue);
281                 }
282                 preconditionLogicStack.peek().addNestedPrecondition(currentPrecondition);
283 
284                 if (currentPrecondition instanceof PreconditionLogic) {
285                     preconditionLogicStack.push(((PreconditionLogic) currentPrecondition));
286                 }
287 
288                 if ("sqlCheck".equals(qName)) {
289                     text = new StringBuffer();
290                 }
291             } else if ("modifySql".equals(qName)) {
292                 inModifySql = true;
293                 if (StringUtils.trimToNull(atts.getValue("dbms")) != null) {
294                     modifySqlDbmsList = new HashSet<String>(StringUtils.splitAndTrim(atts.getValue("dbms"), ","));
295                 }
296                 if (StringUtils.trimToNull(atts.getValue("context")) != null) {
297                     modifySqlContexts = new HashSet<String>(StringUtils.splitAndTrim(atts.getValue("context"), ","));
298                 }
299                 if (StringUtils.trimToNull(atts.getValue("applyToRollback")) != null) {
300                     modifySqlAppliedOnRollback = Boolean.valueOf(atts.getValue("applyToRollback"));
301                 }
302             } else if (inModifySql) {
303                 SqlVisitor sqlVisitor = SqlVisitorFactory.getInstance().create(qName);
304                 for (int i = 0; i < atts.getLength(); i++) {
305                     String attributeName = atts.getQName(i);
306                     String attributeValue = atts.getValue(i);
307                     setProperty(sqlVisitor, attributeName, attributeValue);
308                 }
309                 sqlVisitor.setApplicableDbms(modifySqlDbmsList);
310                 sqlVisitor.setApplyToRollback(modifySqlAppliedOnRollback);
311                 sqlVisitor.setContexts(modifySqlContexts);
312 
313                 changeSet.addSqlVisitor(sqlVisitor);
314             } else if (changeSet != null && change == null) {
315                 change = ChangeFactory.getInstance().create(localName);
316                 if (change == null) {
317                     throw new SAXException("Unknown Liquibase extension: " + localName
318                             + ".  Are you missing a jar from your classpath?");
319                 }
320                 change.setChangeSet(changeSet);
321                 text = new StringBuffer();
322                 if (change == null) {
323                     throw new MigrationFailedException(changeSet, "Unknown change: " + localName);
324                 }
325                 change.setResourceAccessor(resourceAccessor);
326                 if (change instanceof CustomChangeWrapper) {
327                     ((CustomChangeWrapper) change).setClassLoader(resourceAccessor.toClassLoader());
328                 }
329                 for (int i = 0; i < atts.getLength(); i++) {
330                     String attributeName = atts.getLocalName(i);
331                     String attributeValue = atts.getValue(i);
332                     setProperty(change, attributeName, attributeValue);
333                 }
334                 change.init();
335             } else if (change != null && "column".equals(qName)) {
336                 ColumnConfig column;
337                 if (change instanceof LoadDataChange) {
338                     column = new LoadDataColumnConfig();
339                 } else {
340                     column = new ColumnConfig();
341                 }
342                 for (int i = 0; i < atts.getLength(); i++) {
343                     String attributeName = atts.getQName(i);
344                     String attributeValue = atts.getValue(i);
345                     setProperty(column, attributeName, attributeValue);
346                 }
347                 if (change instanceof ChangeWithColumns) {
348                     ((ChangeWithColumns) change).addColumn(column);
349                 } else {
350                     throw new RuntimeException("Unexpected column tag for " + change.getClass().getName());
351                 }
352             } else if (change != null && "constraints".equals(qName)) {
353                 ConstraintsConfig constraints = new ConstraintsConfig();
354                 for (int i = 0; i < atts.getLength(); i++) {
355                     String attributeName = atts.getQName(i);
356                     String attributeValue = atts.getValue(i);
357                     setProperty(constraints, attributeName, attributeValue);
358                 }
359                 ColumnConfig lastColumn = null;
360                 if (change instanceof ChangeWithColumns) {
361                     List<ColumnConfig> columns = ((ChangeWithColumns) change).getColumns();
362                     if (columns != null && columns.size() > 0) {
363                         lastColumn = columns.get(columns.size() - 1);
364                     }
365                 } else {
366                     throw new RuntimeException("Unexpected change: " + change.getClass().getName());
367                 }
368                 if (lastColumn == null) {
369                     throw new RuntimeException("Could not determine column to add constraint to");
370                 }
371                 lastColumn.setConstraints(constraints);
372             } else if ("param".equals(qName)) {
373                 if (change instanceof CustomChangeWrapper) {
374                     if (atts.getValue("value") == null) {
375                         paramName = atts.getValue("name");
376                         text = new StringBuffer();
377                     } else {
378                         ((CustomChangeWrapper) change).setParam(atts.getValue("name"), atts.getValue("value"));
379                     }
380                 } else {
381                     throw new MigrationFailedException(changeSet, "'param' unexpected in " + qName);
382                 }
383             } else if ("where".equals(qName)) {
384                 text = new StringBuffer();
385             } else if ("property".equals(qName)) {
386                 String context = StringUtils.trimToNull(atts.getValue("context"));
387                 String dbms = StringUtils.trimToNull(atts.getValue("dbms"));
388                 if (StringUtils.trimToNull(atts.getValue("file")) == null) {
389                     this.changeLogParameters.set(atts.getValue("name"), atts.getValue("value"), context, dbms);
390                 } else {
391                     Properties props = new Properties();
392                     InputStream propertiesStream = resourceAccessor.getResourceAsStream(atts.getValue("file"));
393                     if (propertiesStream == null) {
394                         log.info("Could not open properties file " + atts.getValue("file"));
395                     } else {
396                         props.load(propertiesStream);
397 
398                         for (Map.Entry entry : props.entrySet()) {
399                             this.changeLogParameters.set(entry.getKey().toString(), entry.getValue().toString(),
400                                     context, dbms);
401                         }
402                     }
403                 }
404             } else if (change instanceof ExecuteShellCommandChange && "arg".equals(qName)) {
405                 ((ExecuteShellCommandChange) change).addArg(atts.getValue("value"));
406             } else if (change != null) {
407                 String creatorMethod = "create" + localName.substring(0, 1).toUpperCase() + localName.substring(1);
408 
409                 Object objectToCreateFrom;
410                 if (changeSubObjects.size() == 0) {
411                     objectToCreateFrom = change;
412                 } else {
413                     objectToCreateFrom = changeSubObjects.peek();
414                 }
415 
416                 Method method;
417                 try {
418                     method = objectToCreateFrom.getClass().getMethod(creatorMethod);
419                 } catch (NoSuchMethodException e) {
420                     throw new MigrationFailedException(changeSet, "Could not find creator method " + creatorMethod
421                             + " for tag: " + qName);
422                 }
423                 Object subObject = method.invoke(objectToCreateFrom);
424                 for (int i = 0; i < atts.getLength(); i++) {
425                     String attributeName = atts.getQName(i);
426                     String attributeValue = atts.getValue(i);
427                     setProperty(subObject, attributeName, attributeValue);
428                 }
429                 changeSubObjects.push(subObject);
430 
431             } else {
432                 throw new MigrationFailedException(changeSet, "Unexpected tag: " + qName);
433             }
434         } catch (Exception e) {
435             log.severe("Error thrown as a SAXException: " + e.getMessage(), e);
436             e.printStackTrace();
437             throw new SAXException(e);
438         }
439     }
440 
441     protected boolean handleIncludedChangeLog(String fileName, boolean isRelativePath, String relativeBaseFileName)
442             throws LiquibaseException {
443         if (!(fileName.endsWith(".xml") || fileName.endsWith(".sql"))) {
444             log.debug(relativeBaseFileName + "/" + fileName + " is not a recognized file type");
445             return false;
446         }
447 
448         if (fileName.equalsIgnoreCase(".svn") || fileName.equalsIgnoreCase("cvs")) {
449             return false;
450         }
451 
452         if (isRelativePath) {
453             // workaround for FilenameUtils.normalize() returning null for relative paths like ../conf/liquibase.xml
454             String tempFile = FilenameUtils.concat(FilenameUtils.getFullPath(relativeBaseFileName), fileName);
455             if (tempFile != null && new File(tempFile).exists() == true) {
456                 fileName = tempFile;
457             } else {
458                 fileName = FilenameUtils.getFullPath(relativeBaseFileName) + fileName;
459             }
460         }
461         DatabaseChangeLog changeLog = ChangeLogParserFactory.getInstance().getParser(fileName, resourceAccessor)
462                 .parse(fileName, changeLogParameters, resourceAccessor);
463         PreconditionContainer preconditions = changeLog.getPreconditions();
464         if (preconditions != null) {
465             if (null == databaseChangeLog.getPreconditions()) {
466                 databaseChangeLog.setPreconditions(new PreconditionContainer());
467             }
468             databaseChangeLog.getPreconditions().addNestedPrecondition(preconditions);
469         }
470         for (ChangeSet changeSet : changeLog.getChangeSets()) {
471             handleChangeSet(changeSet);
472         }
473 
474         return true;
475     }
476 
477     private void setProperty(Object object, String attributeName, String attributeValue) throws IllegalAccessException,
478             InvocationTargetException, CustomChangeException {
479         if (object instanceof CustomChangeWrapper) {
480             if (attributeName.equals("class")) {
481                 ((CustomChangeWrapper) object).setClass(changeLogParameters.expandExpressions(attributeValue));
482             } else {
483                 ((CustomChangeWrapper) object).setParam(attributeName,
484                         changeLogParameters.expandExpressions(attributeValue));
485             }
486         } else {
487             ObjectUtil.setProperty(object, attributeName, changeLogParameters.expandExpressions(attributeValue));
488         }
489     }
490 
491     @Override
492     public void endElement(String uri, String localName, String qName) throws SAXException {
493         String textString = null;
494         if (text != null && text.length() > 0) {
495             textString = changeLogParameters.expandExpressions(StringUtils.trimToNull(text.toString()));
496         }
497 
498         try {
499             if (changeSubObjects.size() > 0) {
500                 changeSubObjects.pop();
501             } else if (rootPrecondition != null) {
502                 if ("preConditions".equals(qName)) {
503                     if (changeSet == null) {
504                         databaseChangeLog.setPreconditions(rootPrecondition);
505                         handlePreCondition(rootPrecondition);
506                     } else {
507                         changeSet.setPreconditions(rootPrecondition);
508                     }
509                     rootPrecondition = null;
510                 } else if ("and".equals(qName)) {
511                     preconditionLogicStack.pop();
512                     currentPrecondition = null;
513                 } else if ("or".equals(qName)) {
514                     preconditionLogicStack.pop();
515                     currentPrecondition = null;
516                 } else if ("not".equals(qName)) {
517                     preconditionLogicStack.pop();
518                     currentPrecondition = null;
519                 } else if (qName.equals("sqlCheck")) {
520                     ((SqlPrecondition) currentPrecondition).setSql(textString);
521                     currentPrecondition = null;
522                 } else if (qName.equals("customPrecondition")) {
523                     ((CustomPreconditionWrapper) currentPrecondition).setClassLoader(resourceAccessor.toClassLoader());
524                     currentPrecondition = null;
525                 }
526 
527             } else if (changeSet != null && "rollback".equals(qName)) {
528                 changeSet.addRollBackSQL(textString);
529                 inRollback = false;
530             } else if (change != null && change instanceof RawSQLChange && "comment".equals(qName)) {
531                 ((RawSQLChange) change).setComments(textString);
532                 text = new StringBuffer();
533             } else if (change != null && "where".equals(qName)) {
534                 if (change instanceof UpdateDataChange) {
535                     ((UpdateDataChange) change).setWhereClause(textString);
536                 } else if (change instanceof DeleteDataChange) {
537                     ((DeleteDataChange) change).setWhereClause(textString);
538                 } else {
539                     throw new RuntimeException("Unexpected change type: " + change.getClass().getName());
540                 }
541                 text = new StringBuffer();
542             } else if (change != null && change instanceof CreateProcedureChange && "comment".equals(qName)) {
543                 ((CreateProcedureChange) change).setComments(textString);
544                 text = new StringBuffer();
545             } else if (change != null && change instanceof CustomChangeWrapper && paramName != null
546                     && "param".equals(qName)) {
547                 ((CustomChangeWrapper) change).setParam(paramName, textString);
548                 text = new StringBuffer();
549                 paramName = null;
550             } else if (changeSet != null && "comment".equals(qName)) {
551                 changeSet.setComments(textString);
552                 text = new StringBuffer();
553             } else if (changeSet != null && "changeSet".equals(qName)) {
554                 handleChangeSet(changeSet);
555                 changeSet = null;
556             } else if (change != null && qName.equals("column") && textString != null) {
557                 if (change instanceof InsertDataChange) {
558                     List<ColumnConfig> columns = ((InsertDataChange) change).getColumns();
559                     columns.get(columns.size() - 1).setValue(textString);
560                 } else if (change instanceof UpdateDataChange) {
561                     List<ColumnConfig> columns = ((UpdateDataChange) change).getColumns();
562                     columns.get(columns.size() - 1).setValue(textString);
563                 } else {
564                     throw new RuntimeException("Unexpected column with text: " + textString);
565                 }
566                 this.text = new StringBuffer();
567             } else if (change != null && localName.equals(change.getChangeMetaData().getName())) {
568                 if (textString != null) {
569                     if (change instanceof RawSQLChange) {
570                         ((RawSQLChange) change).setSql(textString);
571                     } else if (change instanceof CreateProcedureChange) {
572                         ((CreateProcedureChange) change).setProcedureBody(textString);
573                         // } else if (change instanceof AlterViewChange) {
574                         // ((AlterViewChange)
575                         // change).setSelectQuery(textString);
576                     } else if (change instanceof CreateViewChange) {
577                         ((CreateViewChange) change).setSelectQuery(textString);
578                     } else if (change instanceof StopChange) {
579                         ((StopChange) change).setMessage(textString);
580                     } else {
581                         throw new RuntimeException("Unexpected text in " + change.getChangeMetaData().getName());
582                     }
583                 }
584                 text = null;
585                 if (inRollback) {
586                     changeSet.addRollbackChange(change);
587                 } else {
588                     changeSet.addChange(change);
589                 }
590                 change = null;
591             } else if (changeSet != null && "validCheckSum".equals(qName)) {
592                 changeSet.addValidCheckSum(text.toString());
593                 text = null;
594             } else if ("modifySql".equals(qName)) {
595                 inModifySql = false;
596                 modifySqlDbmsList = null;
597                 modifySqlContexts = null;
598                 modifySqlAppliedOnRollback = false;
599             }
600         } catch (Exception e) {
601             log.severe("Error thrown as a SAXException: " + e.getMessage(), e);
602             throw new SAXException(databaseChangeLog.getPhysicalFilePath() + ": " + e.getMessage(), e);
603         }
604     }
605 
606     protected void handlePreCondition(@SuppressWarnings("unused") Precondition precondition) {
607         databaseChangeLog.setPreconditions(rootPrecondition);
608     }
609 
610     protected void handleChangeSet(ChangeSet changeSet) {
611         databaseChangeLog.addChangeSet(changeSet);
612     }
613 
614     @Override
615     public void characters(char ch[], int start, int length) throws SAXException {
616         if (text != null) {
617             text.append(new String(ch, start, length));
618         }
619     }
620 
621     /**
622      * Wrapper for Attributes that expands the value as needed
623      */
624     private class ExpandingAttributes implements Attributes {
625         private Attributes attributes;
626 
627         private ExpandingAttributes(Attributes attributes) {
628             this.attributes = attributes;
629         }
630 
631         @Override
632         public int getLength() {
633             return attributes.getLength();
634         }
635 
636         @Override
637         public String getURI(int index) {
638             return attributes.getURI(index);
639         }
640 
641         @Override
642         public String getLocalName(int index) {
643             return attributes.getLocalName(index);
644         }
645 
646         @Override
647         public String getQName(int index) {
648             return attributes.getQName(index);
649         }
650 
651         @Override
652         public String getType(int index) {
653             return attributes.getType(index);
654         }
655 
656         @Override
657         public String getValue(int index) {
658             return attributes.getValue(index);
659         }
660 
661         @Override
662         public int getIndex(String uri, String localName) {
663             return attributes.getIndex(uri, localName);
664         }
665 
666         @Override
667         public int getIndex(String qName) {
668             return attributes.getIndex(qName);
669         }
670 
671         @Override
672         public String getType(String uri, String localName) {
673             return attributes.getType(uri, localName);
674         }
675 
676         @Override
677         public String getType(String qName) {
678             return attributes.getType(qName);
679         }
680 
681         @Override
682         public String getValue(String uri, String localName) {
683             return changeLogParameters.expandExpressions(attributes.getValue(uri, localName));
684         }
685 
686         @Override
687         public String getValue(String qName) {
688             return changeLogParameters.expandExpressions(attributes.getValue(qName));
689         }
690     }
691 
692     static File extractZipFile(URL resource) throws IOException {
693         String file = resource.getFile();
694         String path = file.split("!")[0];
695         if (path.matches("file:\\/[A-Za-z]:\\/.*")) {
696             path = path.replaceFirst("file:\\/", "");
697         } else {
698             path = path.replaceFirst("file:", "");
699         }
700         path = URLDecoder.decode(path);
701         File zipfile = new File(path);
702 
703         File tempDir = File.createTempFile("liquibase-sax", ".dir");
704         tempDir.delete();
705         tempDir.mkdir();
706         // tempDir.deleteOnExit();
707 
708         JarFile jarFile = new JarFile(zipfile);
709         Enumeration<JarEntry> entries = jarFile.entries();
710         while (entries.hasMoreElements()) {
711             JarEntry entry = entries.nextElement();
712             File entryFile = new File(tempDir, entry.getName());
713             entryFile.mkdirs();
714         }
715 
716         return tempDir;
717     }
718 }