001// Copyright 2011 Leo Przybylski. All rights reserved. 002// 003// Redistribution and use in source and binary forms, with or without modification, are 004// permitted provided that the following conditions are met: 005// 006// 1. Redistributions of source code must retain the above copyright notice, this list of 007// conditions and the following disclaimer. 008// 009// 2. Redistributions in binary form must reproduce the above copyright notice, this list 010// of conditions and the following disclaimer in the documentation and/or other materials 011// provided with the distribution. 012// 013// THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED 014// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 015// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR 016// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 017// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 018// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 019// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 020// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 021// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 022// 023// The views and conclusions contained in the software and documentation are those of the 024// authors and should not be interpreted as representing official policies, either expressed 025// or implied, of Leo Przybylski. 026package org.kualigan.maven.plugins.liquibase; 027 028import org.apache.maven.plugin.AbstractMojo; 029import org.apache.maven.plugin.MojoExecutionException; 030import org.apache.maven.plugin.MojoFailureException; 031import org.apache.maven.plugins.annotations.Component; 032import org.apache.maven.plugins.annotations.Mojo; 033import org.apache.maven.plugins.annotations.Parameter; 034import org.apache.maven.project.MavenProject; 035import org.apache.maven.artifact.manager.WagonManager; 036 037import org.liquibase.maven.plugins.AbstractLiquibaseMojo; 038import org.liquibase.maven.plugins.AbstractLiquibaseUpdateMojo; 039import org.liquibase.maven.plugins.LiquibaseRollback; 040 041import liquibase.Liquibase; 042import liquibase.exception.LiquibaseException; 043import liquibase.serializer.ChangeLogSerializer; 044import liquibase.parser.core.xml.LiquibaseEntityResolver; 045import liquibase.parser.core.xml.XMLChangeLogSAXParser; 046import org.apache.maven.wagon.authentication.AuthenticationInfo; 047 048import liquibase.util.xml.DefaultXmlWriter; 049 050import org.tmatesoft.svn.core.ISVNDirEntryHandler; 051import org.tmatesoft.svn.core.SVNDirEntry; 052import org.tmatesoft.svn.core.SVNDepth; 053import org.tmatesoft.svn.core.SVNException; 054import org.tmatesoft.svn.core.SVNURL; 055import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; 056import org.tmatesoft.svn.core.io.SVNRepository; 057import org.tmatesoft.svn.core.io.SVNRepositoryFactory; 058import org.tmatesoft.svn.core.wc.ISVNOptions; 059import org.tmatesoft.svn.core.wc.SVNWCUtil; 060import org.tmatesoft.svn.core.wc.SVNClientManager; 061import org.tmatesoft.svn.core.wc.SVNWCClient; 062import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; 063 064import org.w3c.dom.*; 065 066import javax.xml.parsers.DocumentBuilder; 067import javax.xml.parsers.DocumentBuilderFactory; 068import javax.xml.parsers.ParserConfigurationException; 069 070import java.lang.reflect.Field; 071import java.lang.reflect.Method; 072 073import java.io.File; 074import java.io.FilenameFilter; 075import java.io.FileNotFoundException; 076import java.io.FileOutputStream; 077import java.io.InputStream; 078import java.io.IOException; 079import java.net.URL; 080import java.util.ArrayList; 081import java.util.Arrays; 082import java.util.Collection; 083import java.util.Iterator; 084import java.util.List; 085import java.util.Properties; 086 087import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD; 088import static org.tmatesoft.svn.core.wc.SVNRevision.WORKING; 089 090/** 091 * Undo for a migration of Liquibase changelogs. Basically, rolls back any changes that were made in the most recent 092 * migration. 093 * 094 * @author Leo Przybylski 095 * @goal rollback 096 */ 097public class RollbackMojo extends LiquibaseRollback { 098 public static final String DEFAULT_CHANGELOG_PATH = "src/main/scripts/changelogs"; 099 public static final String DEFAULT_UPDATE_FILE = "target/changelogs/update.xml"; 100 public static final String DEFAULT_UPDATE_PATH = "target/changelogs/update"; 101 public static final String DEFAULT_LBPROP_PATH = "target/test-classes/liquibase/"; 102 public static final String TEST_ROLLBACK_TAG = "test"; 103 104 /** 105 * Suffix for fields that are representing a default value for a another field. 106 */ 107 private static final String DEFAULT_FIELD_SUFFIX = "Default"; 108 109 /** 110 * The fully qualified name of the driver class to use to connect to the database. 111 * 112 * @parameter expression="${liquibase.driver}" 113 */ 114 protected String driver; 115 116 /** 117 * The Database URL to connect to for executing Liquibase. 118 * 119 * @parameter expression="${liquibase.url}" 120 */ 121 protected String url; 122 123 /** 124 125 The Maven Wagon manager to use when obtaining server authentication details. 126 @component role="org.apache.maven.artifact.manager.WagonManager" 127 @required 128 @readonly 129 */ 130 protected WagonManager wagonManager; 131 /** 132 * The server id in settings.xml to use when authenticating with. 133 * 134 * @parameter expression="${liquibase.server}" 135 */ 136 private String server; 137 138 /** 139 * The database username to use to connect to the specified database. 140 * 141 * @parameter expression="${liquibase.username}" 142 */ 143 protected String username; 144 145 /** 146 * The database password to use to connect to the specified database. 147 * 148 * @parameter expression="${liquibase.password}" 149 */ 150 protected String password; 151 152 /** 153 * Use an empty string as the password for the database connection. This should not be 154 * used along side the {@link #password} setting. 155 * 156 * @parameter expression="${liquibase.emptyPassword}" default-value="false" 157 * @deprecated Use an empty or null value for the password instead. 158 */ 159 protected boolean emptyPassword; 160 161 /** 162 * The default schema name to use the for database connection. 163 * 164 * @parameter expression="${liquibase.defaultSchemaName}" 165 */ 166 protected String defaultSchemaName; 167 168 /** 169 * The class to use as the database object. 170 * 171 * @parameter expression="${liquibase.databaseClass}" 172 */ 173 protected String databaseClass; 174 175 /** 176 * Controls the prompting of users as to whether or not they really want to run the 177 * changes on a database that is not local to the machine that the user is current 178 * executing the plugin on. 179 * 180 * @parameter expression="${liquibase.promptOnNonLocalDatabase}" default-value="true" 181 */ 182 protected boolean promptOnNonLocalDatabase; 183 184 /** 185 * Allows for the maven project artifact to be included in the class loader for 186 * obtaining the Liquibase property and DatabaseChangeLog files. 187 * 188 * @parameter expression="${liquibase.includeArtifact}" default-value="true" 189 */ 190 protected boolean includeArtifact; 191 192 /** 193 * Allows for the maven test output directory to be included in the class loader for 194 * obtaining the Liquibase property and DatabaseChangeLog files. 195 * 196 * @parameter expression="${liquibase.includeTestOutputDirectory}" default-value="true" 197 */ 198 protected boolean includeTestOutputDirectory; 199 200 /** 201 * Controls the verbosity of the output from invoking the plugin. 202 * 203 * @parameter expression="${liquibase.verbose}" default-value="false" 204 * @description Controls the verbosity of the plugin when executing 205 */ 206 protected boolean verbose; 207 208 /** 209 * Controls the level of logging from Liquibase when executing. The value can be 210 * "all", "finest", "finer", "fine", "info", "warning", "severe" or "off". The value is 211 * case insensitive. 212 * 213 * @parameter expression="${liquibase.logging}" default-value="INFO" 214 * @description Controls the verbosity of the plugin when executing 215 */ 216 protected String logging; 217 218 /** 219 * The Liquibase properties file used to configure the Liquibase {@link 220 * liquibase.Liquibase}. 221 * 222 * @parameter expression="${liquibase.propertyFile}" 223 */ 224 protected String propertyFile; 225 226 /** 227 * Flag allowing for the Liquibase properties file to override any settings provided in 228 * the Maven plugin configuration. By default if a property is explicity specified it is 229 * not overridden if it also appears in the properties file. 230 * 231 * @parameter expression="${liquibase.propertyFileWillOverride}" default-value="false" 232 */ 233 protected boolean propertyFileWillOverride; 234 235 /** 236 * Flag for forcing the checksums to be cleared from teh DatabaseChangeLog table. 237 * 238 * @parameter expression="${liquibase.clearCheckSums}" default-value="false" 239 */ 240 protected boolean clearCheckSums; 241 242 /** 243 * List of system properties to pass to the database. 244 * 245 * @parameter 246 */ 247 protected Properties systemProperties; 248 249 /** 250 * @parameter default-value="${project.basedir}/src/main/scripts/changelogs" 251 */ 252 protected File changeLogSavePath; 253 254 /** 255 * @parameter expression="${lb.changeLogTagUrl}" 256 */ 257 protected URL changeLogTagUrl; 258 259 /** 260 * Location of an update.xml 261 * 262 * @parameter expression="${lb.updatePath}" default-value="${project.basedir}/src/main/changelogs" 263 */ 264 protected File updatePath; 265 266 /** 267 * The Maven project that plugin is running under. 268 * @parameter expression="${project}" 269 * @required 270 * @readonly 271 */ 272 @Parameter(property = "project", required = true) 273 protected MavenProject project; 274 275 protected File getBasedir() { 276 return project.getBasedir(); 277 } 278 279 protected void doFieldHack() { 280 for (final Field field : getClass().getDeclaredFields()) { 281 try { 282 final Field parentField = getDeclaredField(getClass().getSuperclass(), field.getName()); 283 if (parentField != null) { 284 getLog().debug("Setting " + field.getName() + " in " + parentField.getDeclaringClass().getName() + " to " + field.get(this)); 285 parentField.set(this, field.get(this)); 286 } 287 } 288 catch (Exception e) { 289 } 290 } 291 } 292 293 protected File[] getLiquibasePropertiesFiles() throws MojoExecutionException { 294 try { 295 final File[] retval = new File(getBasedir(), DEFAULT_LBPROP_PATH).listFiles(new FilenameFilter() { 296 public boolean accept(final File dir, final String name) { 297 return name.endsWith(".properties"); 298 } 299 }); 300 if (retval == null) { 301 throw new NullPointerException(); 302 } 303 return retval; 304 } 305 catch (Exception e) { 306 getLog().warn("Unable to get liquibase properties files "); 307 return new File[0]; 308 // throw new MojoExecutionException("Unable to get liquibase properties files ", e); 309 } 310 } 311 312 @Override 313 public void execute() throws MojoExecutionException, MojoFailureException { 314 doFieldHack(); 315 316 try { 317 Method meth = AbstractLiquibaseMojo.class.getDeclaredMethod("processSystemProperties"); 318 meth.setAccessible(true); 319 meth.invoke(this); 320 } 321 catch (Exception e) { 322 e.printStackTrace(); 323 } 324 325 ClassLoader artifactClassLoader = getMavenArtifactClassLoader(); 326 final File[] propertyFiles = getLiquibasePropertiesFiles(); 327 328 // execute change logs on each database 329 for (final File props : propertyFiles) { 330 try { 331 propertyFile = props.getCanonicalPath(); 332 doFieldHack(); 333 334 configureFieldsAndValues(getFileOpener(artifactClassLoader)); 335 336 doFieldHack(); 337 } 338 catch (Exception e) { 339 throw new MojoExecutionException(e.getMessage(), e); 340 } 341 } 342 343 changeLogFile = new File(changeLogSavePath, "update.xml").getPath(); 344 rollbackCount = 1; 345 346 super.execute(); 347 } 348 349 @Override 350 protected void performLiquibaseTask(Liquibase liquibase) throws LiquibaseException { 351 liquibase.rollback("undo", contexts); 352 } 353 354 355 @Override 356 protected void printSettings(String indent) { 357 super.printSettings(indent); 358 } 359 360 /** 361 * Parses a properties file and sets the assocaited fields in the plugin. 362 * 363 * @param propertiesInputStream The input stream which is the Liquibase properties that 364 * needs to be parsed. 365 * @throws org.apache.maven.plugin.MojoExecutionException 366 * If there is a problem parsing 367 * the file. 368 */ 369 protected void parsePropertiesFile(InputStream propertiesInputStream) 370 throws MojoExecutionException { 371 if (propertiesInputStream == null) { 372 throw new MojoExecutionException("Properties file InputStream is null."); 373 } 374 Properties props = new Properties(); 375 try { 376 props.load(propertiesInputStream); 377 } 378 catch (IOException e) { 379 throw new MojoExecutionException("Could not load the properties Liquibase file", e); 380 } 381 382 for (Iterator it = props.keySet().iterator(); it.hasNext();) { 383 String key = null; 384 try { 385 key = (String) it.next(); 386 Field field = getDeclaredField(this.getClass(), key); 387 388 if (propertyFileWillOverride) { 389 setFieldValue(field, props.get(key).toString()); 390 } else { 391 if (!isCurrentFieldValueSpecified(field)) { 392 getLog().debug(" properties file setting value: " + field.getName()); 393 setFieldValue(field, props.get(key).toString()); 394 } 395 } 396 } 397 catch (Exception e) { 398 getLog().info(" '" + key + "' in properties file is not being used by this " 399 + "task."); 400 } 401 } 402 } 403 404 /** 405 * This method will check to see if the user has specified a value different to that of 406 * the default value. This is not an ideal solution, but should cover most situations in 407 * the use of the plugin. 408 * 409 * @param f The Field to check if a user has specified a value for. 410 * @return <code>true</code> if the user has specified a value. 411 */ 412 private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException { 413 Object currentValue = f.get(this); 414 if (currentValue == null) { 415 return false; 416 } 417 418 Object defaultValue = getDefaultValue(f); 419 if (defaultValue == null) { 420 return currentValue != null; 421 } else { 422 // There is a default value, check to see if the user has selected something other 423 // than the default 424 return !defaultValue.equals(f.get(this)); 425 } 426 } 427 428 private Object getDefaultValue(Field field) throws IllegalAccessException { 429 List<Field> allFields = new ArrayList<Field>(); 430 allFields.addAll(Arrays.asList(getClass().getDeclaredFields())); 431 allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields())); 432 433 for (Field f : allFields) { 434 if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) { 435 f.setAccessible(true); 436 return f.get(this); 437 } 438 } 439 return null; 440 } 441 442 443 /** 444 * Recursively searches for the field specified by the fieldName in the class and all 445 * the super classes until it either finds it, or runs out of parents. 446 * @param clazz The Class to start searching from. 447 * @param fieldName The name of the field to retrieve. 448 * @return The {@link Field} identified by the field name. 449 * @throws NoSuchFieldException If the field was not found in the class or any of its 450 * super classes. 451 */ 452 protected Field getDeclaredField(Class clazz, String fieldName) 453 throws NoSuchFieldException { 454 getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'"); 455 try { 456 Field f = clazz.getDeclaredField(fieldName); 457 458 if (f != null) { 459 return f; 460 } 461 } 462 catch (Exception e) { 463 } 464 465 while (clazz.getSuperclass() != null) { 466 clazz = clazz.getSuperclass(); 467 getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'"); 468 try { 469 Field f = clazz.getDeclaredField(fieldName); 470 471 if (f != null) { 472 return f; 473 } 474 } 475 catch (Exception e) { 476 } 477 } 478 479 throw new NoSuchFieldException("The field '" + fieldName + "' could not be " 480 + "found in the class of any of its parent " 481 + "classes."); 482 } 483 484 private void setFieldValue(Field field, String value) throws IllegalAccessException { 485 if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) { 486 field.set(this, Boolean.valueOf(value)); 487 } else { 488 field.set(this, value); 489 } 490 } 491}