Coverage Report - org.liquibase.maven.plugins.AbstractLiquibaseMojo
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractLiquibaseMojo
25%
42/162
23%
19/80
4.882
 
 1  
 package org.liquibase.maven.plugins;
 2  
 
 3  
 import java.io.IOException;
 4  
 import java.io.InputStream;
 5  
 import java.lang.reflect.Field;
 6  
 import java.net.MalformedURLException;
 7  
 import java.util.*;
 8  
 
 9  
 import liquibase.*;
 10  
 import liquibase.integration.commandline.CommandLineUtils;
 11  
 import liquibase.logging.LogFactory;
 12  
 import liquibase.resource.CompositeResourceAccessor;
 13  
 import liquibase.resource.ResourceAccessor;
 14  
 import liquibase.resource.FileSystemResourceAccessor;
 15  
 import liquibase.database.Database;
 16  
 import liquibase.exception.*;
 17  
 import liquibase.util.ui.UIFactory;
 18  
 import org.apache.maven.artifact.manager.WagonManager;
 19  
 import org.apache.maven.plugin.*;
 20  
 import org.apache.maven.project.MavenProject;
 21  
 import org.apache.maven.wagon.authentication.AuthenticationInfo;
 22  
 
 23  
 /**
 24  
  * A base class for providing Liquibase {@link liquibase.Liquibase} functionality.
 25  
  * 
 26  
  * @author Peter Murray
 27  
  * 
 28  
  *         Test dependency is used because when you run a goal outside the build phases you want to have the same
 29  
  *         dependencies that it would had if it was ran inside test phase
 30  
  * @requiresDependencyResolution test
 31  
  */
 32  10
 public abstract class AbstractLiquibaseMojo extends AbstractMojo {
 33  
 
 34  
     /**
 35  
      * Suffix for fields that are representing a default value for a another field.
 36  
      */
 37  
     private static final String DEFAULT_FIELD_SUFFIX = "Default";
 38  
 
 39  
     /**
 40  
      * The fully qualified name of the driver class to use to connect to the database.
 41  
      * 
 42  
      * @parameter expression="${liquibase.driver}"
 43  
      */
 44  
     protected String driver;
 45  
 
 46  
     /**
 47  
      * The Database URL to connect to for executing Liquibase.
 48  
      * 
 49  
      * @parameter expression="${liquibase.url}"
 50  
      */
 51  
     protected String url;
 52  
 
 53  
     /**
 54  
      * The Maven Wagon manager to use when obtaining server authentication details.
 55  
      * 
 56  
      * @component role="org.apache.maven.artifact.manager.WagonManager"
 57  
      * @required
 58  
      * @readonly
 59  
      */
 60  
     protected WagonManager wagonManager;
 61  
     /**
 62  
      * The server id in settings.xml to use when authenticating with.
 63  
      * 
 64  
      * @parameter expression="${liquibase.server}"
 65  
      */
 66  
     private String server;
 67  
 
 68  
     /**
 69  
      * The database username to use to connect to the specified database.
 70  
      * 
 71  
      * @parameter expression="${liquibase.username}"
 72  
      */
 73  
     protected String username;
 74  
 
 75  
     /**
 76  
      * The database password to use to connect to the specified database.
 77  
      * 
 78  
      * @parameter expression="${liquibase.password}"
 79  
      */
 80  
     protected String password;
 81  
 
 82  
     /**
 83  
      * Use an empty string as the password for the database connection. This should not be used along side the
 84  
      * {@link #password} setting.
 85  
      * 
 86  
      * @parameter expression="${liquibase.emptyPassword}" default-value="false"
 87  
      * @deprecated Use an empty or null value for the password instead.
 88  
      */
 89  
     protected boolean emptyPassword;
 90  
 
 91  
     /**
 92  
      * The default schema name to use the for database connection.
 93  
      * 
 94  
      * @parameter expression="${liquibase.defaultSchemaName}"
 95  
      */
 96  
     protected String defaultSchemaName;
 97  
 
 98  
     /**
 99  
      * The class to use as the database object.
 100  
      * 
 101  
      * @parameter expression="${liquibase.databaseClass}"
 102  
      */
 103  
     protected String databaseClass;
 104  
 
 105  
     /**
 106  
      * Controls the prompting of users as to whether or not they really want to run the changes on a database that is
 107  
      * not local to the machine that the user is current executing the plugin on.
 108  
      * 
 109  
      * @parameter expression="${liquibase.promptOnNonLocalDatabase}" default-value="true"
 110  
      */
 111  
     protected boolean promptOnNonLocalDatabase;
 112  
 
 113  
     /**
 114  
      * Allows for the maven project artifact to be included in the class loader for obtaining the Liquibase property and
 115  
      * DatabaseChangeLog files.
 116  
      * 
 117  
      * @parameter expression="${liquibase.includeArtifact}" default-value="true"
 118  
      */
 119  
     protected boolean includeArtifact;
 120  
 
 121  
     /**
 122  
      * Allows for the maven test output directory to be included in the class loader for obtaining the Liquibase
 123  
      * property and DatabaseChangeLog files.
 124  
      * 
 125  
      * @parameter expression="${liquibase.includeTestOutputDirectory}" default-value="true"
 126  
      */
 127  
     protected boolean includeTestOutputDirectory;
 128  
 
 129  
     /**
 130  
      * Controls the verbosity of the output from invoking the plugin.
 131  
      * 
 132  
      * @parameter expression="${liquibase.verbose}" default-value="false"
 133  
      * @description Controls the verbosity of the plugin when executing
 134  
      */
 135  
     protected boolean verbose;
 136  
 
 137  
     /**
 138  
      * Controls the level of logging from Liquibase when executing. The value can be "all", "finest", "finer", "fine",
 139  
      * "info", "warning", "severe" or "off". The value is case insensitive.
 140  
      * 
 141  
      * @parameter expression="${liquibase.logging}" default-value="INFO"
 142  
      * @description Controls the verbosity of the plugin when executing
 143  
      */
 144  
     protected String logging;
 145  
 
 146  
     /**
 147  
      * The Liquibase properties file used to configure the Liquibase {@link liquibase.Liquibase}.
 148  
      * 
 149  
      * @parameter expression="${liquibase.propertyFile}"
 150  
      */
 151  
     protected String propertyFile;
 152  
 
 153  
     /**
 154  
      * Flag allowing for the Liquibase properties file to override any settings provided in the Maven plugin
 155  
      * configuration. By default if a property is explicity specified it is not overridden if it also appears in the
 156  
      * properties file.
 157  
      * 
 158  
      * @parameter expression="${liquibase.propertyFileWillOverride}" default-value="false"
 159  
      */
 160  
     protected boolean propertyFileWillOverride;
 161  
 
 162  
     /**
 163  
      * Flag for forcing the checksums to be cleared from teh DatabaseChangeLog table.
 164  
      * 
 165  
      * @parameter expression="${liquibase.clearCheckSums}" default-value="false"
 166  
      */
 167  
     protected boolean clearCheckSums;
 168  
 
 169  
     /**
 170  
      * List of system properties to pass to the database.
 171  
      * 
 172  
      * @parameter
 173  
      */
 174  
     protected Properties systemProperties;
 175  
 
 176  
     /**
 177  
      * The Maven project that plugin is running under.
 178  
      * 
 179  
      * @parameter expression="${project}"
 180  
      * @required
 181  
      * @readonly
 182  
      */
 183  
     protected MavenProject project;
 184  
 
 185  
     /**
 186  
      * The {@link Liquibase} object used modify the database.
 187  
      */
 188  
     private Liquibase liquibase;
 189  
 
 190  
     /**
 191  
      * Array to put a expression variable to maven plugin.
 192  
      * 
 193  
      * @parameter
 194  
      */
 195  
     private Properties expressionVars;
 196  
 
 197  
     /**
 198  
      * Set this to 'true' to skip running liquibase. Its use is NOT RECOMMENDED, but quite convenient on occasion.
 199  
      * 
 200  
      * @parameter expression="${liquibase.should.run}"
 201  
      */
 202  
     protected boolean skip;
 203  
 
 204  
     /**
 205  
      * Array to put a expression variable to maven plugin.
 206  
      * 
 207  
      * @parameter
 208  
      */
 209  
     private Map expressionVariables;
 210  
 
 211  
     public void execute() throws MojoExecutionException, MojoFailureException {
 212  0
         getLog().info(MavenUtils.LOG_SEPARATOR);
 213  
 
 214  0
         if (server != null) {
 215  0
             AuthenticationInfo info = wagonManager.getAuthenticationInfo(server);
 216  0
             if (info != null) {
 217  0
                 username = info.getUserName();
 218  0
                 password = info.getPassword();
 219  
             }
 220  
         }
 221  
 
 222  0
         String shouldRunProperty = System.getProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY);
 223  0
         if (shouldRunProperty != null && !Boolean.valueOf(shouldRunProperty)) {
 224  0
             getLog().warn(
 225  
                     "Liquibase did not run because '" + Liquibase.SHOULD_RUN_SYSTEM_PROPERTY
 226  
                             + "' system property was set to false");
 227  0
             return;
 228  
         }
 229  
 
 230  0
         if (skip) {
 231  0
             getLog().warn("Liquibase skipped due to maven configuration");
 232  0
             return;
 233  
         }
 234  
 
 235  0
         processSystemProperties();
 236  
 
 237  0
         ClassLoader artifactClassLoader = getMavenArtifactClassLoader();
 238  0
         configureFieldsAndValues(getFileOpener(artifactClassLoader));
 239  
 
 240  
         try {
 241  0
             LogFactory.setLoggingLevel(logging);
 242  0
         } catch (IllegalArgumentException e) {
 243  0
             throw new MojoExecutionException("Failed to set logging level: " + e.getMessage(), e);
 244  0
         }
 245  
 
 246  
         // Displays the settings for the Mojo depending of verbosity mode.
 247  0
         displayMojoSettings();
 248  
 
 249  
         // Check that all the parameters that must be specified have been by the user.
 250  0
         checkRequiredParametersAreSpecified();
 251  
 
 252  0
         Database database = null;
 253  
         try {
 254  0
             String dbPassword = emptyPassword || password == null ? "" : password;
 255  0
             database = CommandLineUtils.createDatabaseObject(artifactClassLoader, url, username, dbPassword, driver,
 256  
                     defaultSchemaName, databaseClass, null);
 257  0
             liquibase = createLiquibase(getFileOpener(artifactClassLoader), database);
 258  
 
 259  0
             getLog().debug("expressionVars = " + String.valueOf(expressionVars));
 260  
 
 261  0
             if (expressionVars != null) {
 262  0
                 for (Map.Entry<Object, Object> var : expressionVars.entrySet()) {
 263  0
                     this.liquibase.setChangeLogParameter(var.getKey().toString(), var.getValue());
 264  
                 }
 265  
             }
 266  
 
 267  0
             getLog().debug("expressionVariables = " + String.valueOf(expressionVariables));
 268  0
             if (expressionVariables != null) {
 269  0
                 for (Map.Entry var : (Set<Map.Entry>) expressionVariables.entrySet()) {
 270  0
                     if (var.getValue() != null) {
 271  0
                         this.liquibase.setChangeLogParameter(var.getKey().toString(), var.getValue());
 272  
                     }
 273  
                 }
 274  
             }
 275  
 
 276  0
             if (clearCheckSums) {
 277  0
                 getLog().info("Clearing the Liquibase Checksums on the database");
 278  0
                 liquibase.clearCheckSums();
 279  
             }
 280  
 
 281  0
             getLog().info("Executing on Database: " + url);
 282  
 
 283  0
             if (isPromptOnNonLocalDatabase()) {
 284  0
                 if (!liquibase.isSafeToRunMigration()) {
 285  0
                     if (UIFactory.getInstance().getFacade().promptForNonLocalDatabase(liquibase.getDatabase())) {
 286  0
                         throw new LiquibaseException("User decided not to run against non-local database");
 287  
                     }
 288  
                 }
 289  
             }
 290  
 
 291  0
             performLiquibaseTask(liquibase);
 292  0
         } catch (LiquibaseException e) {
 293  0
             cleanup(database);
 294  0
             throw new MojoExecutionException("Error setting up or running Liquibase: " + e.getMessage(), e);
 295  0
         }
 296  
 
 297  0
         cleanup(database);
 298  0
         getLog().info(MavenUtils.LOG_SEPARATOR);
 299  0
         getLog().info("");
 300  0
     }
 301  
 
 302  
     protected Liquibase getLiquibase() {
 303  0
         return liquibase;
 304  
     }
 305  
 
 306  
     protected abstract void performLiquibaseTask(Liquibase liquibase) throws LiquibaseException;
 307  
 
 308  
     protected boolean isPromptOnNonLocalDatabase() {
 309  0
         return promptOnNonLocalDatabase;
 310  
     }
 311  
 
 312  
     private void displayMojoSettings() {
 313  0
         if (verbose) {
 314  0
             getLog().info("Settings----------------------------");
 315  0
             printSettings("    ");
 316  0
             getLog().info(MavenUtils.LOG_SEPARATOR);
 317  
         }
 318  0
     }
 319  
 
 320  
     protected Liquibase createLiquibase(ResourceAccessor fo, Database db) throws MojoExecutionException {
 321  
         try {
 322  0
             return new Liquibase("", fo, db);
 323  0
         } catch (LiquibaseException ex) {
 324  0
             throw new MojoExecutionException("Error creating liquibase: " + ex.getMessage(), ex);
 325  
         }
 326  
     }
 327  
 
 328  
     public void configureFieldsAndValues(ResourceAccessor fo) throws MojoExecutionException, MojoFailureException {
 329  
         // Load the properties file if there is one, but only for values that the user has not
 330  
         // already specified.
 331  6
         if (propertyFile != null) {
 332  3
             getLog().info("Parsing Liquibase Properties File");
 333  3
             getLog().info("  File: " + propertyFile);
 334  
             try {
 335  3
                 InputStream is = fo.getResourceAsStream(propertyFile);
 336  3
                 if (is == null) {
 337  0
                     throw new MojoFailureException("Failed to resolve the properties file.");
 338  
                 }
 339  3
                 parsePropertiesFile(is);
 340  3
                 getLog().info(MavenUtils.LOG_SEPARATOR);
 341  0
             } catch (IOException e) {
 342  0
                 throw new MojoExecutionException("Failed to resolve properties file", e);
 343  3
             }
 344  
         }
 345  6
     }
 346  
 
 347  
     protected ClassLoader getMavenArtifactClassLoader() throws MojoExecutionException {
 348  
         try {
 349  0
             return MavenUtils.getArtifactClassloader(project, includeArtifact, includeTestOutputDirectory, getClass(),
 350  
                     getLog(), verbose);
 351  0
         } catch (MalformedURLException e) {
 352  0
             throw new MojoExecutionException("Failed to create artifact classloader", e);
 353  
         }
 354  
     }
 355  
 
 356  
     protected ResourceAccessor getFileOpener(ClassLoader cl) {
 357  0
         ResourceAccessor mFO = new MavenResourceAccessor(cl);
 358  0
         ResourceAccessor fsFO = new FileSystemResourceAccessor(project.getBasedir().getAbsolutePath());
 359  0
         return new CompositeResourceAccessor(mFO, fsFO);
 360  
     }
 361  
 
 362  
     /**
 363  
      * Performs some validation after the properties file has been loaded checking that all properties required have
 364  
      * been specified.
 365  
      * 
 366  
      * @throws MojoFailureException
 367  
      *             If any property that is required has not been specified.
 368  
      */
 369  
     protected void checkRequiredParametersAreSpecified() throws MojoFailureException {
 370  0
         if (driver == null) {
 371  0
             throw new MojoFailureException("The driver has not been specified either as a "
 372  
                     + "parameter or in a properties file.");
 373  0
         } else if (url == null) {
 374  0
             throw new MojoFailureException("The database URL has not been specified either as "
 375  
                     + "a parameter or in a properties file.");
 376  
         }
 377  
 
 378  0
         if (password != null && emptyPassword) {
 379  0
             throw new MojoFailureException("A password cannot be present and the empty "
 380  
                     + "password property both be specified.");
 381  
         }
 382  0
     }
 383  
 
 384  
     /**
 385  
      * Prints the settings that have been set of defaulted for the plugin. These will only be shown in verbose mode.
 386  
      * 
 387  
      * @param indent
 388  
      *            The indent string to use when printing the settings.
 389  
      */
 390  
     protected void printSettings(String indent) {
 391  0
         if (indent == null) {
 392  0
             indent = "";
 393  
         }
 394  0
         getLog().info(indent + "driver: " + driver);
 395  0
         getLog().info(indent + "url: " + url);
 396  0
         getLog().info(indent + "username: " + username);
 397  0
         getLog().info(indent + "password: " + password);
 398  0
         getLog().info(indent + "use empty password: " + emptyPassword);
 399  0
         getLog().info(indent + "properties file: " + propertyFile);
 400  0
         getLog().info(indent + "properties file will override? " + propertyFileWillOverride);
 401  0
         getLog().info(indent + "prompt on non-local database? " + promptOnNonLocalDatabase);
 402  0
         getLog().info(indent + "clear checksums? " + clearCheckSums);
 403  0
     }
 404  
 
 405  
     protected void cleanup(Database db) {
 406  
         // Release any locks that we may have on the database.
 407  0
         if (getLiquibase() != null) {
 408  
             try {
 409  0
                 getLiquibase().forceReleaseLocks();
 410  0
             } catch (LiquibaseException e) {
 411  0
                 getLog().error(e.getMessage(), e);
 412  0
             }
 413  
         }
 414  
 
 415  
         // Clean up the connection
 416  0
         if (db != null) {
 417  
             try {
 418  0
                 db.rollback();
 419  0
                 db.close();
 420  0
             } catch (DatabaseException e) {
 421  0
                 getLog().error("Failed to close open connection to database.", e);
 422  0
             }
 423  
         }
 424  0
     }
 425  
 
 426  
     /**
 427  
      * Parses a properties file and sets the assocaited fields in the plugin.
 428  
      * 
 429  
      * @param propertiesInputStream
 430  
      *            The input stream which is the Liquibase properties that needs to be parsed.
 431  
      * @throws org.apache.maven.plugin.MojoExecutionException
 432  
      *             If there is a problem parsing the file.
 433  
      */
 434  
     protected void parsePropertiesFile(InputStream propertiesInputStream) throws MojoExecutionException {
 435  3
         if (propertiesInputStream == null) {
 436  0
             throw new MojoExecutionException("Properties file InputStream is null.");
 437  
         }
 438  3
         Properties props = new Properties();
 439  
         try {
 440  3
             props.load(propertiesInputStream);
 441  0
         } catch (IOException e) {
 442  0
             throw new MojoExecutionException("Could not load the properties Liquibase file", e);
 443  3
         }
 444  
 
 445  3
         for (Iterator it = props.keySet().iterator(); it.hasNext();) {
 446  13
             String key = null;
 447  
             try {
 448  13
                 key = (String) it.next();
 449  13
                 Field field = MavenUtils.getDeclaredField(this.getClass(), key);
 450  
 
 451  13
                 if (propertyFileWillOverride) {
 452  9
                     getLog().debug("  properties file setting value: " + field.getName());
 453  9
                     setFieldValue(field, props.get(key).toString());
 454  
                 } else {
 455  4
                     if (!isCurrentFieldValueSpecified(field)) {
 456  1
                         getLog().debug("  properties file setting value: " + field.getName());
 457  1
                         setFieldValue(field, props.get(key).toString());
 458  
                     }
 459  
                 }
 460  0
             } catch (Exception e) {
 461  0
                 getLog().info("  '" + key + "' in properties file is not being used by this " + "task.");
 462  13
             }
 463  13
         }
 464  3
     }
 465  
 
 466  
     /**
 467  
      * This method will check to see if the user has specified a value different to that of the default value. This is
 468  
      * not an ideal solution, but should cover most situations in the use of the plugin.
 469  
      * 
 470  
      * @param f
 471  
      *            The Field to check if a user has specified a value for.
 472  
      * @return <code>true</code> if the user has specified a value.
 473  
      */
 474  
     private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException {
 475  4
         Object currentValue = f.get(this);
 476  4
         if (currentValue == null) {
 477  1
             return false;
 478  
         }
 479  
 
 480  3
         Object defaultValue = getDefaultValue(f);
 481  3
         if (defaultValue == null) {
 482  3
             return currentValue != null;
 483  
         } else {
 484  
             // There is a default value, check to see if the user has selected something other
 485  
             // than the default
 486  0
             return !defaultValue.equals(f.get(this));
 487  
         }
 488  
     }
 489  
 
 490  
     private Object getDefaultValue(Field field) throws IllegalAccessException {
 491  3
         List<Field> allFields = new ArrayList<Field>();
 492  3
         allFields.addAll(Arrays.asList(getClass().getDeclaredFields()));
 493  3
         allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields()));
 494  
 
 495  3
         for (Field f : allFields) {
 496  75
             if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) {
 497  0
                 f.setAccessible(true);
 498  0
                 return f.get(this);
 499  
             }
 500  
         }
 501  3
         return null;
 502  
     }
 503  
 
 504  
     private void setFieldValue(Field field, String value) throws IllegalAccessException {
 505  10
         if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) {
 506  0
             field.set(this, Boolean.valueOf(value));
 507  
         } else {
 508  10
             field.set(this, value);
 509  
         }
 510  10
     }
 511  
 
 512  
     @SuppressWarnings("unchecked")
 513  
     private void processSystemProperties() {
 514  0
         if (systemProperties == null) {
 515  0
             systemProperties = new Properties();
 516  
         }
 517  
         // Add all system properties configured by the user
 518  0
         Iterator iter = systemProperties.keySet().iterator();
 519  0
         while (iter.hasNext()) {
 520  0
             String key = (String) iter.next();
 521  0
             String value = systemProperties.getProperty(key);
 522  0
             System.setProperty(key, value);
 523  0
         }
 524  0
     }
 525  
 
 526  
 }