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.project.MavenProject; 032import org.apache.maven.artifact.manager.WagonManager; 033 034import org.liquibase.maven.plugins.AbstractLiquibaseMojo; 035import org.liquibase.maven.plugins.AbstractLiquibaseUpdateMojo; 036import org.liquibase.maven.plugins.LiquibaseRollback; 037 038import liquibase.Liquibase; 039import liquibase.exception.LiquibaseException; 040import liquibase.serializer.ChangeLogSerializer; 041import liquibase.parser.core.xml.LiquibaseEntityResolver; 042import liquibase.parser.core.xml.XMLChangeLogSAXParser; 043import org.apache.maven.wagon.authentication.AuthenticationInfo; 044 045import liquibase.util.xml.DefaultXmlWriter; 046 047import org.tmatesoft.svn.core.ISVNDirEntryHandler; 048import org.tmatesoft.svn.core.SVNDirEntry; 049import org.tmatesoft.svn.core.SVNDepth; 050import org.tmatesoft.svn.core.SVNException; 051import org.tmatesoft.svn.core.SVNURL; 052import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; 053import org.tmatesoft.svn.core.io.SVNRepository; 054import org.tmatesoft.svn.core.io.SVNRepositoryFactory; 055import org.tmatesoft.svn.core.wc.ISVNOptions; 056import org.tmatesoft.svn.core.wc.SVNWCUtil; 057import org.tmatesoft.svn.core.wc.SVNClientManager; 058import org.tmatesoft.svn.core.wc.SVNWCClient; 059import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; 060 061import org.w3c.dom.*; 062 063import javax.xml.parsers.DocumentBuilder; 064import javax.xml.parsers.DocumentBuilderFactory; 065import javax.xml.parsers.ParserConfigurationException; 066 067import java.lang.reflect.Field; 068import java.lang.reflect.Method; 069 070import java.io.File; 071import java.io.FileNotFoundException; 072import java.io.FileOutputStream; 073import java.io.InputStream; 074import java.io.IOException; 075import java.net.URL; 076import java.util.ArrayList; 077import java.util.Arrays; 078import java.util.Collection; 079import java.util.Iterator; 080import java.util.List; 081import java.util.Properties; 082 083import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD; 084import static org.tmatesoft.svn.core.wc.SVNRevision.WORKING; 085 086/** 087 * Undo for a migration of Liquibase changelogs. Basically, rolls back any changes that were made in the most recent 088 * migration. 089 * 090 * @author Leo Przybylski 091 * @goal rollback 092 */ 093public class RollbackMojo extends LiquibaseRollback { 094 public static final String DEFAULT_CHANGELOG_PATH = "src/main/changelogs"; 095 096 /** 097 * Suffix for fields that are representing a default value for a another field. 098 */ 099 private static final String DEFAULT_FIELD_SUFFIX = "Default"; 100 101 /** 102 * The fully qualified name of the driver class to use to connect to the database. 103 * 104 * @parameter expression="${liquibase.driver}" 105 */ 106 protected String driver; 107 108 /** 109 * The Database URL to connect to for executing Liquibase. 110 * 111 * @parameter expression="${liquibase.url}" 112 */ 113 protected String url; 114 115 /** 116 117 The Maven Wagon manager to use when obtaining server authentication details. 118 @component role="org.apache.maven.artifact.manager.WagonManager" 119 @required 120 @readonly 121 */ 122 protected WagonManager wagonManager; 123 /** 124 * The server id in settings.xml to use when authenticating with. 125 * 126 * @parameter expression="${liquibase.server}" 127 */ 128 private String server; 129 130 /** 131 * The database username to use to connect to the specified database. 132 * 133 * @parameter expression="${liquibase.username}" 134 */ 135 protected String username; 136 137 /** 138 * The database password to use to connect to the specified database. 139 * 140 * @parameter expression="${liquibase.password}" 141 */ 142 protected String password; 143 144 /** 145 * Use an empty string as the password for the database connection. This should not be 146 * used along side the {@link #password} setting. 147 * 148 * @parameter expression="${liquibase.emptyPassword}" default-value="false" 149 * @deprecated Use an empty or null value for the password instead. 150 */ 151 protected boolean emptyPassword; 152 153 /** 154 * The default schema name to use the for database connection. 155 * 156 * @parameter expression="${liquibase.defaultSchemaName}" 157 */ 158 protected String defaultSchemaName; 159 160 /** 161 * The class to use as the database object. 162 * 163 * @parameter expression="${liquibase.databaseClass}" 164 */ 165 protected String databaseClass; 166 167 /** 168 * Controls the prompting of users as to whether or not they really want to run the 169 * changes on a database that is not local to the machine that the user is current 170 * executing the plugin on. 171 * 172 * @parameter expression="${liquibase.promptOnNonLocalDatabase}" default-value="true" 173 */ 174 protected boolean promptOnNonLocalDatabase; 175 176 /** 177 * Allows for the maven project artifact to be included in the class loader for 178 * obtaining the Liquibase property and DatabaseChangeLog files. 179 * 180 * @parameter expression="${liquibase.includeArtifact}" default-value="true" 181 */ 182 protected boolean includeArtifact; 183 184 /** 185 * Allows for the maven test output directory to be included in the class loader for 186 * obtaining the Liquibase property and DatabaseChangeLog files. 187 * 188 * @parameter expression="${liquibase.includeTestOutputDirectory}" default-value="true" 189 */ 190 protected boolean includeTestOutputDirectory; 191 192 /** 193 * Controls the verbosity of the output from invoking the plugin. 194 * 195 * @parameter expression="${liquibase.verbose}" default-value="false" 196 * @description Controls the verbosity of the plugin when executing 197 */ 198 protected boolean verbose; 199 200 /** 201 * Controls the level of logging from Liquibase when executing. The value can be 202 * "all", "finest", "finer", "fine", "info", "warning", "severe" or "off". The value is 203 * case insensitive. 204 * 205 * @parameter expression="${liquibase.logging}" default-value="INFO" 206 * @description Controls the verbosity of the plugin when executing 207 */ 208 protected String logging; 209 210 /** 211 * The Liquibase properties file used to configure the Liquibase {@link 212 * liquibase.Liquibase}. 213 * 214 * @parameter expression="${liquibase.propertyFile}" 215 */ 216 protected String propertyFile; 217 218 /** 219 * Flag allowing for the Liquibase properties file to override any settings provided in 220 * the Maven plugin configuration. By default if a property is explicity specified it is 221 * not overridden if it also appears in the properties file. 222 * 223 * @parameter expression="${liquibase.propertyFileWillOverride}" default-value="false" 224 */ 225 protected boolean propertyFileWillOverride; 226 227 /** 228 * Flag for forcing the checksums to be cleared from teh DatabaseChangeLog table. 229 * 230 * @parameter expression="${liquibase.clearCheckSums}" default-value="false" 231 */ 232 protected boolean clearCheckSums; 233 234 /** 235 * List of system properties to pass to the database. 236 * 237 * @parameter 238 */ 239 protected Properties systemProperties; 240 241 protected File getBasedir() { 242 return project.getBasedir(); 243 } 244 245 protected void doFieldHack() { 246 for (final Field field : getClass().getDeclaredFields()) { 247 try { 248 final Field parentField = getDeclaredField(getClass().getSuperclass(), field.getName()); 249 if (parentField != null) { 250 getLog().debug("Setting " + field.getName() + " in " + parentField.getDeclaringClass().getName() + " to " + field.get(this)); 251 parentField.set(this, field.get(this)); 252 } 253 } 254 catch (Exception e) { 255 } 256 } 257 } 258 259 @Override 260 public void execute() throws MojoExecutionException, MojoFailureException { 261 doFieldHack(); 262 263 try { 264 Method meth = AbstractLiquibaseMojo.class.getDeclaredMethod("processSystemProperties"); 265 meth.setAccessible(true); 266 meth.invoke(this); 267 } 268 catch (Exception e) { 269 e.printStackTrace(); 270 } 271 272 ClassLoader artifactClassLoader = getMavenArtifactClassLoader(); 273 configureFieldsAndValues(getFileOpener(artifactClassLoader)); 274 275 doFieldHack(); 276 277 super.execute(); 278 } 279 280 @Override 281 protected void performLiquibaseTask(Liquibase liquibase) throws LiquibaseException { 282 liquibase.rollback("undo", contexts); 283 } 284 285 286 @Override 287 protected void printSettings(String indent) { 288 super.printSettings(indent); 289 } 290 291 /** 292 * Parses a properties file and sets the assocaited fields in the plugin. 293 * 294 * @param propertiesInputStream The input stream which is the Liquibase properties that 295 * needs to be parsed. 296 * @throws org.apache.maven.plugin.MojoExecutionException 297 * If there is a problem parsing 298 * the file. 299 */ 300 protected void parsePropertiesFile(InputStream propertiesInputStream) 301 throws MojoExecutionException { 302 if (propertiesInputStream == null) { 303 throw new MojoExecutionException("Properties file InputStream is null."); 304 } 305 Properties props = new Properties(); 306 try { 307 props.load(propertiesInputStream); 308 } 309 catch (IOException e) { 310 throw new MojoExecutionException("Could not load the properties Liquibase file", e); 311 } 312 313 for (Iterator it = props.keySet().iterator(); it.hasNext();) { 314 String key = null; 315 try { 316 key = (String) it.next(); 317 Field field = getDeclaredField(this.getClass(), key); 318 319 if (propertyFileWillOverride) { 320 setFieldValue(field, props.get(key).toString()); 321 } else { 322 if (!isCurrentFieldValueSpecified(field)) { 323 getLog().debug(" properties file setting value: " + field.getName()); 324 setFieldValue(field, props.get(key).toString()); 325 } 326 } 327 } 328 catch (Exception e) { 329 getLog().info(" '" + key + "' in properties file is not being used by this " 330 + "task."); 331 } 332 } 333 } 334 335 /** 336 * This method will check to see if the user has specified a value different to that of 337 * the default value. This is not an ideal solution, but should cover most situations in 338 * the use of the plugin. 339 * 340 * @param f The Field to check if a user has specified a value for. 341 * @return <code>true</code> if the user has specified a value. 342 */ 343 private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException { 344 Object currentValue = f.get(this); 345 if (currentValue == null) { 346 return false; 347 } 348 349 Object defaultValue = getDefaultValue(f); 350 if (defaultValue == null) { 351 return currentValue != null; 352 } else { 353 // There is a default value, check to see if the user has selected something other 354 // than the default 355 return !defaultValue.equals(f.get(this)); 356 } 357 } 358 359 private Object getDefaultValue(Field field) throws IllegalAccessException { 360 List<Field> allFields = new ArrayList<Field>(); 361 allFields.addAll(Arrays.asList(getClass().getDeclaredFields())); 362 allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields())); 363 364 for (Field f : allFields) { 365 if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) { 366 f.setAccessible(true); 367 return f.get(this); 368 } 369 } 370 return null; 371 } 372 373 374 /** 375 * Recursively searches for the field specified by the fieldName in the class and all 376 * the super classes until it either finds it, or runs out of parents. 377 * @param clazz The Class to start searching from. 378 * @param fieldName The name of the field to retrieve. 379 * @return The {@link Field} identified by the field name. 380 * @throws NoSuchFieldException If the field was not found in the class or any of its 381 * super classes. 382 */ 383 protected Field getDeclaredField(Class clazz, String fieldName) 384 throws NoSuchFieldException { 385 getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'"); 386 try { 387 Field f = clazz.getDeclaredField(fieldName); 388 389 if (f != null) { 390 return f; 391 } 392 } 393 catch (Exception e) { 394 } 395 396 while (clazz.getSuperclass() != null) { 397 clazz = clazz.getSuperclass(); 398 getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'"); 399 try { 400 Field f = clazz.getDeclaredField(fieldName); 401 402 if (f != null) { 403 return f; 404 } 405 } 406 catch (Exception e) { 407 } 408 } 409 410 throw new NoSuchFieldException("The field '" + fieldName + "' could not be " 411 + "found in the class of any of its parent " 412 + "classes."); 413 } 414 415 private void setFieldValue(Field field, String value) throws IllegalAccessException { 416 if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) { 417 field.set(this, Boolean.valueOf(value)); 418 } else { 419 field.set(this, value); 420 } 421 } 422}