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 028 029import org.apache.maven.plugin.AbstractMojo; 030import org.apache.maven.plugin.MojoExecutionException; 031import org.apache.maven.plugin.MojoFailureException; 032import org.apache.maven.plugins.annotations.Component; 033import org.apache.maven.plugins.annotations.Mojo; 034import org.apache.maven.plugins.annotations.Parameter; 035import org.apache.maven.project.MavenProject; 036import org.apache.maven.artifact.manager.WagonManager; 037 038import org.liquibase.maven.plugins.MavenUtils; 039import org.liquibase.maven.plugins.AbstractLiquibaseMojo; 040import org.liquibase.maven.plugins.AbstractLiquibaseChangeLogMojo; 041import org.liquibase.maven.plugins.MavenResourceAccessor; 042 043import org.apache.maven.shared.invoker.DefaultInvocationRequest; 044import org.apache.maven.shared.invoker.DefaultInvoker; 045import org.apache.maven.shared.invoker.InvocationOutputHandler; 046import org.apache.maven.shared.invoker.InvocationRequest; 047import org.apache.maven.shared.invoker.InvocationResult; 048import org.apache.maven.shared.invoker.Invoker; 049import org.apache.maven.shared.invoker.InvokerLogger; 050import org.apache.maven.shared.invoker.MavenInvocationException; 051 052import org.apache.commons.cli.CommandLine; 053import org.apache.commons.cli.OptionBuilder; 054import org.apache.commons.cli.Options; 055import org.apache.commons.cli.PosixParser; 056 057import org.codehaus.plexus.util.FileUtils; 058import org.codehaus.plexus.util.IOUtil; 059import org.codehaus.plexus.util.StringUtils; 060import org.codehaus.plexus.util.cli.CommandLineUtils; 061 062import liquibase.Liquibase; 063import liquibase.database.Database; 064import liquibase.database.DatabaseFactory; 065import liquibase.database.core.H2Database; 066import liquibase.database.jvm.JdbcConnection; 067import liquibase.exception.LiquibaseException; 068import liquibase.logging.LogFactory; 069import liquibase.serializer.ChangeLogSerializer; 070import liquibase.parser.core.xml.LiquibaseEntityResolver; 071import liquibase.parser.core.xml.XMLChangeLogSAXParser; 072import liquibase.resource.CompositeResourceAccessor; 073import liquibase.resource.FileSystemResourceAccessor; 074import liquibase.resource.ResourceAccessor; 075 076import org.apache.maven.wagon.authentication.AuthenticationInfo; 077 078import liquibase.util.xml.DefaultXmlWriter; 079 080import org.kualigan.tools.liquibase.Diff; 081import org.kualigan.tools.liquibase.DiffResult; 082 083import org.w3c.dom.*; 084 085import javax.xml.parsers.DocumentBuilder; 086import javax.xml.parsers.DocumentBuilderFactory; 087import javax.xml.parsers.ParserConfigurationException; 088 089import java.lang.reflect.Field; 090import java.lang.reflect.Method; 091 092import java.io.File; 093import java.io.FileNotFoundException; 094import java.io.FileOutputStream; 095import java.io.InputStream; 096import java.io.IOException; 097import java.net.URL; 098import java.sql.Connection; 099import java.sql.DriverManager; 100import java.sql.Statement; 101import java.util.ArrayList; 102import java.util.Arrays; 103import java.util.Collection; 104import java.util.Iterator; 105import java.util.List; 106import java.util.Properties; 107import java.util.StringTokenizer; 108 109/** 110 * Copies a database including DDL/DML from one location to another. 111 * 112 * @author Leo Przybylski 113 */ 114 @Mojo( 115 name="copy-database", 116 requiresProject = false 117 ) 118public class CopyMojo extends AbstractLiquibaseChangeLogMojo { 119 public static final String DEFAULT_CHANGELOG_PATH = "src/main/changelogs"; 120 121 /** 122 * Suffix for fields that are representing a default value for a another field. 123 */ 124 private static final String DEFAULT_FIELD_SUFFIX = "Default"; 125 126 private static final Options OPTIONS = new Options(); 127 128 private static final char SET_SYSTEM_PROPERTY = 'D'; 129 130 private static final char OFFLINE = 'o'; 131 132 private static final char REACTOR = 'r'; 133 134 private static final char QUIET = 'q'; 135 136 private static final char DEBUG = 'X'; 137 138 private static final char ERRORS = 'e'; 139 140 private static final char NON_RECURSIVE = 'N'; 141 142 private static final char UPDATE_SNAPSHOTS = 'U'; 143 144 private static final char ACTIVATE_PROFILES = 'P'; 145 146 private static final String FORCE_PLUGIN_UPDATES = "cpu"; 147 148 private static final String FORCE_PLUGIN_UPDATES2 = "up"; 149 150 private static final String SUPPRESS_PLUGIN_UPDATES = "npu"; 151 152 private static final String SUPPRESS_PLUGIN_REGISTRY = "npr"; 153 154 private static final char CHECKSUM_FAILURE_POLICY = 'C'; 155 156 private static final char CHECKSUM_WARNING_POLICY = 'c'; 157 158 private static final char ALTERNATE_USER_SETTINGS = 's'; 159 160 private static final String FAIL_FAST = "ff"; 161 162 private static final String FAIL_AT_END = "fae"; 163 164 private static final String FAIL_NEVER = "fn"; 165 166 private static final String ALTERNATE_POM_FILE = "f"; 167 168 169 @Parameter(property = "project", defaultValue = "${project}") 170 protected MavenProject project; 171 172 /** 173 * User settings use to check the interactiveMode. 174 * 175 */ 176 @Parameter(property = "interactiveMode", defaultValue = "${settings.interactiveMode}") 177 protected Boolean interactiveMode; 178 179 /** 180 * 181 * The Maven Wagon manager to use when obtaining server authentication details. 182 */ 183 @Component(role=org.apache.maven.artifact.manager.WagonManager.class) 184 protected WagonManager wagonManager; 185 186 /** 187 * 188 * The Maven Wagon manager to use when obtaining server authentication details. 189 */ 190 @Component(role=org.kualigan.maven.plugins.liquibase.MigrateHelper.class) 191 protected MigrateHelper migrator; 192 193 /** 194 * The server id in settings.xml to use when authenticating the source server with. 195 */ 196 @Parameter(property = "lb.copy.source", required = true) 197 private String source; 198 199 /** 200 * The server id in settings.xml to use when authenticating the source server with. 201 */ 202 @Parameter(property = "lb.copy.source.schema") 203 private String sourceSchema; 204 205 private String sourceUser; 206 207 private String sourcePass; 208 209 /** 210 * The server id in settings.xml to use when authenticating the source server with. 211 */ 212 @Parameter(property = "lb.copy.source.driver") 213 private String sourceDriverClass; 214 215 /** 216 * The server id in settings.xml to use when authenticating the source server with. 217 */ 218 @Parameter(property = "lb.copy.source.url", required = true) 219 private String sourceUrl; 220 221 /** 222 * The server id in settings.xml to use when authenticating the target server with. 223 */ 224 @Parameter(property = "lb.copy.target", required = true) 225 private String target; 226 227 /** 228 * The server id in settings.xml to use when authenticating the target server with. 229 */ 230 @Parameter(property = "lb.copy.target.schema") 231 private String targetSchema; 232 233 private String targetUser; 234 235 private String targetPass; 236 237 /** 238 * The server id in settings.xml to use when authenticating the source server with. 239 */ 240 @Parameter(property = "lb.copy.target.driver") 241 private String targetDriverClass; 242 243 /** 244 * The server id in settings.xml to use when authenticating the source server with. 245 */ 246 @Parameter(property = "lb.copy.target.url", required = true) 247 private String targetUrl; 248 249 250 /** 251 * Controls the verbosity of the output from invoking the plugin. 252 * 253 * @description Controls the verbosity of the plugin when executing 254 */ 255 @Parameter(property = "liquibase.verbose", defaultValue = "false") 256 protected boolean verbose; 257 258 /** 259 * Controls the level of logging from Liquibase when executing. The value can be 260 * "all", "finest", "finer", "fine", "info", "warning", "severe" or "off". The value is 261 * case insensitive. 262 * 263 * @description Controls the verbosity of the plugin when executing 264 */ 265 @Parameter(property = "liquibase.logging", defaultValue = "INFO") 266 protected String logging; 267 268 /** 269 * The Liquibase properties file used to configure the Liquibase {@link 270 * liquibase.Liquibase}. 271 */ 272 @Parameter(property = "liquibase.propertyFile") 273 protected String propertyFile; 274 275 /** 276 * Specifies the change log file to use for Liquibase. No longer needed with updatePath. 277 * @deprecated 278 */ 279 @Parameter(property = "liquibase.changeLogFile") 280 protected String changeLogFile; 281 282 /** 283 */ 284 @Parameter(property = "liquibase.changeLogSavePath", defaultValue = "${project.basedir}/target/changelogs") 285 protected File changeLogSavePath; 286 287 /** 288 * Whether or not to perform a drop on the database before executing the change. 289 */ 290 @Parameter(property = "liquibase.dropFirst", defaultValue = "false") 291 protected boolean dropFirst; 292 293 /** 294 * Property to flag whether to copy data as well as structure of the database schema 295 */ 296 @Parameter(property = "lb.copy.data", defaultValue = "true") 297 protected boolean stateSaved; 298 299 protected Boolean isStateSaved() { 300 return stateSaved; 301 } 302 303 /** 304 * The {@code M2_HOME} parameter to use for forked Maven invocations. 305 * 306 */ 307 @Parameter(defaultValue = "${maven.home}") 308 protected File mavenHome; 309 310 protected File getBasedir() { 311 return project.getBasedir(); 312 } 313 314 protected String getChangeLogFile() throws MojoExecutionException { 315 if (changeLogFile != null) { 316 return changeLogFile; 317 } 318 319 try { 320 changeLogFile = changeLogSavePath.getCanonicalPath(); 321 new File(changeLogFile).mkdirs(); 322 changeLogFile += File.separator + targetUser; 323 return changeLogFile; 324 } 325 catch (Exception e) { 326 throw new MojoExecutionException("Exception getting the location of the change log file: " + e.getMessage(), e); 327 } 328 } 329 330 protected void doFieldHack() { 331 for (final Field field : getClass().getDeclaredFields()) { 332 try { 333 final Field parentField = getDeclaredField(getClass().getSuperclass(), field.getName()); 334 if (parentField != null) { 335 getLog().debug("Setting " + field.getName() + " in " + parentField.getDeclaringClass().getName() + " to " + field.get(this)); 336 parentField.set(this, field.get(this)); 337 } 338 } 339 catch (Exception e) { 340 } 341 } 342 } 343 344 345 /* 346 @Override 347 public void execute() throws MojoExecutionException, MojoFailureException { 348 doFieldHack(); 349 350 try { 351 Method meth = AbstractLiquibaseMojo.class.getDeclaredMethod("processSystemProperties"); 352 meth.setAccessible(true); 353 meth.invoke(this); 354 } 355 catch (Exception e) { 356 e.printStackTrace(); 357 } 358 359 ClassLoader artifactClassLoader = getMavenArtifactClassLoader(); 360 configureFieldsAndValues(getFileOpener(artifactClassLoader)); 361 362 doFieldHack(); 363 364 365 super.execute(); 366 } 367 */ 368 369 public ClassLoader getMavenArtifactClassloader() throws MojoExecutionException { 370 try { 371 return MavenUtils.getArtifactClassloader(project, true, false, getClass(), getLog(), false); 372 } 373 catch (Exception e) { 374 throw new MojoExecutionException(e.getMessage(), e); 375 } 376 } 377 378 public String lookupDriverFor(final String url) { 379 for (final Database databaseImpl : DatabaseFactory.getInstance().getImplementedDatabases()) { 380 final String driver = databaseImpl.getDefaultDriver(url); 381 if (driver != null) { 382 return driver; 383 } 384 } 385 return null; 386 } 387 388 public void execute() throws MojoExecutionException, MojoFailureException { 389 if (project == null || project.getArtifactId().equalsIgnoreCase("standalone-pom")) { 390 getLog().info("Using standalone-pom. No project. I have to create one."); 391 generateArchetype(getMavenHome(), new Properties() {{ 392 setProperty("archetypeGroupId", "org.kualigan.maven.archetypes"); 393 setProperty("archetypeArtifactId", "lb-copy-archetype"); 394 setProperty("archetypeVersion", "1.1.7"); 395 setProperty("groupId", "org.kualigan.liquibase"); 396 setProperty("artifactId", "copy"); 397 setProperty("version", "1.0.0-SNAPSHOT"); 398 }}); 399 400 invokeCopy(getMavenHome(), new Properties() {{ 401 setProperty("lb.copy.source", source); 402 setProperty("lb.copy.source.url", sourceUrl); 403 if (sourceDriverClass != null) { 404 setProperty("lb.copy.source.driver", sourceDriverClass); 405 } 406 if (sourceSchema != null) { 407 setProperty("lb.copy.source.schema", sourceSchema); 408 } 409 setProperty("lb.copy.target", target); 410 setProperty("lb.copy.target.url", targetUrl); 411 if (targetDriverClass != null) { 412 setProperty("lb.copy.target.driver", targetDriverClass); 413 } 414 if (targetSchema != null) { 415 setProperty("lb.copy.target.schema", targetSchema); 416 } 417 }}); 418 419 } 420 else { 421 doCopy(); 422 } 423 } 424 425 /** 426 * Invokes the maven goal {@code archetype:generate} with the appropriate properties. 427 * 428 */ 429 public void invokeCopy(final File mavenHome, final Properties copyProperties) throws MojoExecutionException { 430 final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome); 431 invoker.setWorkingDirectory(new File(System.getProperty("user.dir") + File.separator + "copy")); 432 433 final String additionalArguments = ""; 434 435 final InvocationRequest req = new DefaultInvocationRequest() 436 .setInteractive(true) 437 .setProperties(copyProperties); 438 439 setupRequest(req, additionalArguments); 440 441 req.setGoals(new ArrayList<String>() {{ add("lb:copy-database"); }}); 442 443 try { 444 final InvocationResult invocationResult = invoker.execute(req); 445 446 if ( invocationResult.getExecutionException() != null ) { 447 throw new MojoExecutionException("Error executing Maven.", 448 invocationResult.getExecutionException()); 449 } 450 451 if (invocationResult.getExitCode() != 0) { 452 throw new MojoExecutionException( 453 "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'"); 454 } 455 } 456 catch (MavenInvocationException e) { 457 throw new MojoExecutionException( "Failed to invoke Maven build.", e ); 458 } 459 } 460 461 /** 462 * Invokes the maven goal {@code archetype:generate} with the appropriate properties. 463 * 464 */ 465 public void generateArchetype(final File mavenHome, final Properties archetypeProperties) throws MojoExecutionException { 466 final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome); 467 468 final String additionalArguments = ""; 469 470 final InvocationRequest req = new DefaultInvocationRequest() 471 .setInteractive(false) 472 .setProperties(archetypeProperties); 473 474 setupRequest(req, additionalArguments); 475 476 req.setGoals(new ArrayList<String>() {{ add("archetype:generate"); }}); 477 478 try { 479 final InvocationResult invocationResult = invoker.execute(req); 480 481 if ( invocationResult.getExecutionException() != null ) { 482 throw new MojoExecutionException("Error executing Maven.", 483 invocationResult.getExecutionException()); 484 } 485 486 if (invocationResult.getExitCode() != 0) { 487 throw new MojoExecutionException( 488 "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'"); 489 } 490 } 491 catch (MavenInvocationException e) { 492 throw new MojoExecutionException( "Failed to invoke Maven build.", e ); 493 } 494 } 495 496 /** 497 * 498 */ 499 protected void setupRequest(final InvocationRequest req, 500 final String additionalArguments) throws MojoExecutionException { 501 try { 502 final String[] args = CommandLineUtils.translateCommandline(additionalArguments); 503 CommandLine cli = new PosixParser().parse(OPTIONS, args); 504 505 if (cli.hasOption( SET_SYSTEM_PROPERTY)) { 506 String[] properties = cli.getOptionValues(SET_SYSTEM_PROPERTY); 507 Properties props = new Properties(); 508 for ( int i = 0; i < properties.length; i++ ) 509 { 510 String property = properties[i]; 511 String name, value; 512 int sep = property.indexOf( "=" ); 513 if ( sep <= 0 ) 514 { 515 name = property.trim(); 516 value = "true"; 517 } 518 else 519 { 520 name = property.substring( 0, sep ).trim(); 521 value = property.substring( sep + 1 ).trim(); 522 } 523 props.setProperty( name, value ); 524 } 525 526 req.setProperties( props ); 527 } 528 529 if ( cli.hasOption( OFFLINE ) ) 530 { 531 req.setOffline( true ); 532 } 533 534 if ( cli.hasOption( QUIET ) ) 535 { 536 // TODO: setQuiet() currently not supported by InvocationRequest 537 req.setDebug( false ); 538 } 539 else if ( cli.hasOption( DEBUG ) ) 540 { 541 req.setDebug( true ); 542 } 543 else if ( cli.hasOption( ERRORS ) ) 544 { 545 req.setShowErrors( true ); 546 } 547 548 if ( cli.hasOption( REACTOR ) ) 549 { 550 req.setRecursive( true ); 551 } 552 else if ( cli.hasOption( NON_RECURSIVE ) ) 553 { 554 req.setRecursive( false ); 555 } 556 557 if ( cli.hasOption( UPDATE_SNAPSHOTS ) ) 558 { 559 req.setUpdateSnapshots( true ); 560 } 561 562 if ( cli.hasOption( ACTIVATE_PROFILES ) ) 563 { 564 String[] profiles = cli.getOptionValues( ACTIVATE_PROFILES ); 565 List<String> activatedProfiles = new ArrayList<String>(); 566 List<String> deactivatedProfiles = new ArrayList<String>(); 567 568 if ( profiles != null ) 569 { 570 for ( int i = 0; i < profiles.length; ++i ) 571 { 572 StringTokenizer profileTokens = new StringTokenizer( profiles[i], "," ); 573 574 while ( profileTokens.hasMoreTokens() ) 575 { 576 String profileAction = profileTokens.nextToken().trim(); 577 578 if ( profileAction.startsWith( "-" ) || profileAction.startsWith( "!" ) ) 579 { 580 deactivatedProfiles.add( profileAction.substring( 1 ) ); 581 } 582 else if ( profileAction.startsWith( "+" ) ) 583 { 584 activatedProfiles.add( profileAction.substring( 1 ) ); 585 } 586 else 587 { 588 activatedProfiles.add( profileAction ); 589 } 590 } 591 } 592 } 593 594 if ( !deactivatedProfiles.isEmpty() ) 595 { 596 getLog().warn( "Explicit profile deactivation is not yet supported. " 597 + "The following profiles will NOT be deactivated: " + StringUtils.join( 598 deactivatedProfiles.iterator(), ", " ) ); 599 } 600 601 if ( !activatedProfiles.isEmpty() ) 602 { 603 req.setProfiles( activatedProfiles ); 604 } 605 } 606 607 if ( cli.hasOption( FORCE_PLUGIN_UPDATES ) || cli.hasOption( FORCE_PLUGIN_UPDATES2 ) ) 608 { 609 getLog().warn( "Forcing plugin updates is not supported currently." ); 610 } 611 else if ( cli.hasOption( SUPPRESS_PLUGIN_UPDATES ) ) 612 { 613 req.setNonPluginUpdates( true ); 614 } 615 616 if ( cli.hasOption( SUPPRESS_PLUGIN_REGISTRY ) ) 617 { 618 getLog().warn( "Explicit suppression of the plugin registry is not supported currently." ); 619 } 620 621 if ( cli.hasOption( CHECKSUM_FAILURE_POLICY ) ) 622 { 623 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_FAIL ); 624 } 625 else if ( cli.hasOption( CHECKSUM_WARNING_POLICY ) ) 626 { 627 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_WARN ); 628 } 629 630 if ( cli.hasOption( ALTERNATE_USER_SETTINGS ) ) 631 { 632 req.setUserSettingsFile( new File( cli.getOptionValue( ALTERNATE_USER_SETTINGS ) ) ); 633 } 634 635 if ( cli.hasOption( FAIL_AT_END ) ) 636 { 637 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_AT_END ); 638 } 639 else if ( cli.hasOption( FAIL_FAST ) ) 640 { 641 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_FAST ); 642 } 643 if ( cli.hasOption( FAIL_NEVER ) ) 644 { 645 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_NEVER ); 646 } 647 if ( cli.hasOption( ALTERNATE_POM_FILE ) ) 648 { 649 if ( req.getPomFileName() != null ) { 650 getLog().info("pomFileName is already set, ignoring the -f argument" ); 651 } 652 else { 653 req.setPomFileName(cli.getOptionValue(ALTERNATE_POM_FILE)); 654 } 655 } 656 } 657 catch (Exception e) { 658 throw new MojoExecutionException("Failed to re-parse additional arguments for Maven invocation.", e ); 659 } 660 } 661 662 protected void doCopy() throws MojoExecutionException, MojoFailureException { 663 getLog().info(MavenUtils.LOG_SEPARATOR); 664 665 if (source != null) { 666 AuthenticationInfo info = wagonManager.getAuthenticationInfo(source); 667 if (info != null) { 668 sourceUser = info.getUserName(); 669 sourcePass = info.getPassword(); 670 } 671 } 672 673 sourceDriverClass = lookupDriverFor(sourceUrl); 674 675 if (sourceSchema == null) { 676 sourceSchema = sourceUser; 677 } 678 679 if (target != null) { 680 AuthenticationInfo info = wagonManager.getAuthenticationInfo(target); 681 if (info != null) { 682 targetUser = info.getUserName(); 683 targetPass = info.getPassword(); 684 } 685 } 686 687 if (targetSchema == null) { 688 targetSchema = targetUser; 689 } 690 691 targetDriverClass = lookupDriverFor(targetUrl); 692 693 final String shouldRunProperty = System.getProperty(Liquibase.SHOULD_RUN_SYSTEM_PROPERTY); 694 if (shouldRunProperty != null && !Boolean.valueOf(shouldRunProperty)) { 695 getLog().info("Liquibase did not run because '" + Liquibase.SHOULD_RUN_SYSTEM_PROPERTY 696 + "' system property was set to false"); 697 return; 698 } 699 700 if (skip) { 701 getLog().warn("Liquibase skipped due to maven configuration"); 702 return; 703 } 704 705 getLog().info("project " + project); 706 707 // processSystemProperties(); 708 final ClassLoader artifactClassLoader = getMavenArtifactClassloader(); 709 // configureFieldsAndValues(getFileOpener(artifactClassLoader)); 710 711 try { 712 LogFactory.setLoggingLevel(logging); 713 } 714 catch (IllegalArgumentException e) { 715 throw new MojoExecutionException("Failed to set logging level: " + e.getMessage(), 716 e); 717 } 718 719 // Displays the settings for the Mojo depending of verbosity mode. 720 // displayMojoSettings(); 721 722 // Check that all the parameters that must be specified have been by the user. 723 //checkRequiredParametersAreSpecified(); 724 725 726 final Database lbSource = createSourceDatabase(); 727 final Database lbTarget = createTargetDatabase(); 728 729 try { 730 exportSchema(lbSource, lbTarget); 731 updateSchema(lbTarget); 732 733 if (isStateSaved()) { 734 getLog().info("Starting data load from schema " + sourceSchema); 735 migrator.migrate(lbSource, lbTarget, getLog(), interactiveMode); 736 // exportData(lbSource, lbTarget); 737 } 738 739 try { 740 updateConstraints(lbTarget, artifactClassLoader); 741 } 742 catch (Exception e) { 743 // Squash errors for constraints 744 } 745 746 if (lbTarget instanceof H2Database) { 747 final Statement st = ((JdbcConnection) lbTarget.getConnection()).createStatement(); 748 st.execute("SHUTDOWN DEFRAG"); 749 } 750 751 } 752 catch (Exception e) { 753 throw new MojoExecutionException(e.getMessage(), e); 754 } 755 finally { 756 try { 757 if (lbSource != null) { 758 lbSource.close(); 759 } 760 if (lbTarget != null) { 761 lbTarget.close(); 762 } 763 } 764 catch (Exception e) { 765 } 766 } 767 768 769 cleanup(lbSource); 770 cleanup(lbTarget); 771 772 getLog().info(MavenUtils.LOG_SEPARATOR); 773 getLog().info(""); 774 } 775 776 protected void updateSchema(final Database target) throws MojoExecutionException { 777 final ClassLoader artifactClassLoader = getMavenArtifactClassloader(); 778 updateTables (target, artifactClassLoader); 779 updateSequences(target, artifactClassLoader); 780 updateViews (target, artifactClassLoader); 781 updateIndexes (target, artifactClassLoader); 782 } 783 784 protected void updateTables(final Database target, final ClassLoader artifactClassLoader) throws MojoExecutionException { 785 try { 786 final Liquibase liquibase = new Liquibase(getChangeLogFile() + "-tab.xml", getFileOpener(artifactClassLoader), target); 787 liquibase.update(null); 788 } 789 catch (Exception e) { 790 throw new MojoExecutionException(e.getMessage(), e); 791 } 792 793 } 794 795 protected void updateSequences(final Database target, final ClassLoader artifactClassLoader) throws MojoExecutionException { 796 try { 797 final Liquibase liquibase = new Liquibase(getChangeLogFile() + "-seq.xml", getFileOpener(artifactClassLoader), target); 798 liquibase.update(null); 799 } 800 catch (Exception e) { 801 throw new MojoExecutionException(e.getMessage(), e); 802 } 803 } 804 805 protected void updateViews(final Database target, final ClassLoader artifactClassLoader) throws MojoExecutionException { 806 try { 807 final Liquibase liquibase = new Liquibase(getChangeLogFile() + "-vw.xml", getFileOpener(artifactClassLoader), target); 808 liquibase.update(null); 809 } 810 catch (Exception e) { 811 throw new MojoExecutionException(e.getMessage(), e); 812 } 813 } 814 815 protected void updateIndexes(final Database target, final ClassLoader artifactClassLoader) throws MojoExecutionException { 816 try { 817 final Liquibase liquibase = new Liquibase(getChangeLogFile() + "-idx.xml", getFileOpener(artifactClassLoader), target); 818 liquibase.update(null); 819 } 820 catch (Exception e) { 821 throw new MojoExecutionException(e.getMessage(), e); 822 } 823 } 824 825 protected void updateConstraints(final Database target, final ClassLoader artifactClassLoader) throws MojoExecutionException { 826 try { 827 final Liquibase liquibase = new Liquibase(getChangeLogFile() + "-cst.xml", getFileOpener(artifactClassLoader), target); 828 liquibase.update(null); 829 } 830 catch (Exception e) { 831 throw new MojoExecutionException(e.getMessage(), e); 832 } 833 } 834 835 protected Database createSourceDatabase() throws MojoExecutionException { 836 try { 837 final DatabaseFactory factory = DatabaseFactory.getInstance(); 838 final Database retval = factory.findCorrectDatabaseImplementation(openConnection(sourceUrl, sourceUser, sourcePass, sourceDriverClass, "")); 839 retval.setDefaultSchemaName(sourceSchema); 840 return retval; 841 } 842 catch (Exception e) { 843 throw new MojoExecutionException(e.getMessage(), e); 844 } 845 } 846 847 protected Database createTargetDatabase() throws MojoExecutionException { 848 try { 849 final DatabaseFactory factory = DatabaseFactory.getInstance(); 850 final Database retval = factory.findCorrectDatabaseImplementation(openConnection(targetUrl, targetUser, targetPass, targetDriverClass, "")); 851 retval.setDefaultSchemaName(targetSchema); 852 return retval; 853 } 854 catch (Exception e) { 855 throw new MojoExecutionException(e.getMessage(), e); 856 } 857 } 858 859 /** 860 * Drops the database. Makes sure it's done right the first time. 861 * 862 * @param liquibase 863 * @throws LiquibaseException 864 */ 865 protected void dropAll(final Liquibase liquibase) throws LiquibaseException { 866 boolean retry = true; 867 while (retry) { 868 try { 869 liquibase.dropAll(); 870 retry = false; 871 } 872 catch (LiquibaseException e2) { 873 getLog().info(e2.getMessage()); 874 if (e2.getMessage().indexOf("ORA-02443") < 0 && e2.getCause() != null && retry) { 875 retry = (e2.getCause().getMessage().indexOf("ORA-02443") > -1); 876 } 877 878 if (!retry) { 879 throw e2; 880 } 881 else { 882 getLog().info("Got ORA-2443. Retrying..."); 883 } 884 } 885 } 886 } 887 888 @Override 889 protected void printSettings(String indent) { 890 super.printSettings(indent); 891 getLog().info(indent + "drop first? " + dropFirst); 892 893 } 894 895 /** 896 * Parses a properties file and sets the assocaited fields in the plugin. 897 * 898 * @param propertiesInputStream The input stream which is the Liquibase properties that 899 * needs to be parsed. 900 * @throws org.apache.maven.plugin.MojoExecutionException 901 * If there is a problem parsing 902 * the file. 903 */ 904 protected void parsePropertiesFile(InputStream propertiesInputStream) 905 throws MojoExecutionException { 906 if (propertiesInputStream == null) { 907 throw new MojoExecutionException("Properties file InputStream is null."); 908 } 909 Properties props = new Properties(); 910 try { 911 props.load(propertiesInputStream); 912 } 913 catch (IOException e) { 914 throw new MojoExecutionException("Could not load the properties Liquibase file", e); 915 } 916 917 for (Iterator it = props.keySet().iterator(); it.hasNext();) { 918 String key = null; 919 try { 920 key = (String) it.next(); 921 Field field = getDeclaredField(this.getClass(), key); 922 923 if (propertyFileWillOverride) { 924 setFieldValue(field, props.get(key).toString()); 925 } 926 else { 927 if (!isCurrentFieldValueSpecified(field)) { 928 getLog().debug(" properties file setting value: " + field.getName()); 929 setFieldValue(field, props.get(key).toString()); 930 } 931 } 932 } 933 catch (Exception e) { 934 getLog().info(" '" + key + "' in properties file is not being used by this " 935 + "task."); 936 } 937 } 938 } 939 940 /** 941 * This method will check to see if the user has specified a value different to that of 942 * the default value. This is not an ideal solution, but should cover most situations in 943 * the use of the plugin. 944 * 945 * @param f The Field to check if a user has specified a value for. 946 * @return <code>true</code> if the user has specified a value. 947 */ 948 private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException { 949 Object currentValue = f.get(this); 950 if (currentValue == null) { 951 return false; 952 } 953 954 Object defaultValue = getDefaultValue(f); 955 if (defaultValue == null) { 956 return currentValue != null; 957 } else { 958 // There is a default value, check to see if the user has selected something other 959 // than the default 960 return !defaultValue.equals(f.get(this)); 961 } 962 } 963 964 private Object getDefaultValue(Field field) throws IllegalAccessException { 965 List<Field> allFields = new ArrayList<Field>(); 966 allFields.addAll(Arrays.asList(getClass().getDeclaredFields())); 967 allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields())); 968 969 for (Field f : allFields) { 970 if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) { 971 f.setAccessible(true); 972 return f.get(this); 973 } 974 } 975 return null; 976 } 977 978 979 /** 980 * Recursively searches for the field specified by the fieldName in the class and all 981 * the super classes until it either finds it, or runs out of parents. 982 * @param clazz The Class to start searching from. 983 * @param fieldName The name of the field to retrieve. 984 * @return The {@link Field} identified by the field name. 985 * @throws NoSuchFieldException If the field was not found in the class or any of its 986 * super classes. 987 */ 988 protected Field getDeclaredField(Class clazz, String fieldName) 989 throws NoSuchFieldException { 990 getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'"); 991 try { 992 Field f = clazz.getDeclaredField(fieldName); 993 994 if (f != null) { 995 return f; 996 } 997 } 998 catch (Exception e) { 999 } 1000 1001 while (clazz.getSuperclass() != null) { 1002 clazz = clazz.getSuperclass(); 1003 getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'"); 1004 try { 1005 Field f = clazz.getDeclaredField(fieldName); 1006 1007 if (f != null) { 1008 return f; 1009 } 1010 } 1011 catch (Exception e) { 1012 } 1013 } 1014 1015 throw new NoSuchFieldException("The field '" + fieldName + "' could not be " 1016 + "found in the class of any of its parent " 1017 + "classes."); 1018 } 1019 1020 private void setFieldValue(Field field, String value) throws IllegalAccessException { 1021 if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) { 1022 field.set(this, Boolean.valueOf(value)); 1023 } 1024 else { 1025 field.set(this, value); 1026 } 1027 } 1028 1029/* 1030 protected void exportData(final Database source, final Database target) { 1031 1032 final DatabaseFactory factory = DatabaseFactory.getInstance(); 1033 try { 1034 h2db = factory.findCorrectDatabaseImplementation(new JdbcConnection(openConnection("h2"))); 1035 h2db.setDefaultSchemaName(h2Config.getSchema()); 1036 1037 export(new Diff(source, getDefaultSchemaName()), h2db, "tables", "-dat.xml"); 1038 1039 ResourceAccessor antFO = new AntResourceAccessor(getProject(), classpath); 1040 ResourceAccessor fsFO = new FileSystemResourceAccessor(); 1041 1042 String changeLogFile = getChangeLogFile() + "-dat.xml"; 1043 1044 Liquibase liquibase = new Liquibase(changeLogFile, new CompositeResourceAccessor(antFO, fsFO), h2db); 1045 1046 log("Loading Schema"); 1047 liquibase.update(getContexts()); 1048 log("Finished Loading the Schema"); 1049 1050 } 1051 catch (Exception e) { 1052 } 1053 catch (Exception e) { 1054 throw new BuildException(e); 1055 } 1056 finally { 1057 try { 1058 if (h2db != null) { 1059 // hsqldb.getConnection().createStatement().execute("SHUTDOWN"); 1060 log("Closing h2 database"); 1061 h2db.close(); 1062 } 1063 } 1064 catch (Exception e) { 1065 if (!(e instanceof java.sql.SQLNonTransientConnectionException)) { 1066 e.printStackTrace(); 1067 } 1068 } 1069 1070 } 1071 } 1072 */ 1073 1074 protected void exportConstraints(Diff diff, Database target) throws MojoExecutionException { 1075 export(diff, target, "foreignKeys", "-cst.xml"); 1076 } 1077 1078 protected void exportIndexes(Diff diff, Database target) throws MojoExecutionException { 1079 export(diff, target, "indexes", "-idx.xml"); 1080 } 1081 1082 protected void exportViews(Diff diff, Database target) throws MojoExecutionException { 1083 export(diff, target, "views", "-vw.xml"); 1084 } 1085 1086 protected void exportTables(Diff diff, Database target) throws MojoExecutionException { 1087 export(diff, target, "tables, primaryKeys, uniqueConstraints", "-tab.xml"); 1088 } 1089 1090 protected void exportSequences(Diff diff, Database target) throws MojoExecutionException { 1091 export(diff, target, "sequences", "-seq.xml"); 1092 } 1093 1094 protected void export(final Diff diff, final Database target, final String diffTypes, final String suffix) throws MojoExecutionException { 1095 diff.setDiffTypes(diffTypes); 1096 1097 try { 1098 DiffResult results = diff.compare(); 1099 results.printChangeLog(getChangeLogFile() + suffix, target); 1100 } 1101 catch (Exception e) { 1102 throw new MojoExecutionException("Exception while exporting to the target: " + e.getMessage(), e); 1103 } 1104 } 1105 1106 protected void exportSchema(final Database source, final Database target) throws MojoExecutionException { 1107 try { 1108 Diff diff = new Diff(source, source.getDefaultSchemaName()); 1109 exportTables(diff, target); 1110 exportSequences(diff, target); 1111 exportViews(diff, target); 1112 exportIndexes(diff, target); 1113 exportConstraints(diff, target); 1114 } 1115 catch (Exception e) { 1116 throw new MojoExecutionException("Exception while exporting the source schema: " + e.getMessage(), e); 1117 } 1118 } 1119 1120 protected JdbcConnection openConnection(final String url, 1121 final String username, 1122 final String password, 1123 final String className, 1124 final String schema) throws MojoExecutionException { 1125 Connection retval = null; 1126 int retry_count = 0; 1127 final int max_retry = 5; 1128 while (retry_count < max_retry) { 1129 try { 1130 getLog().debug("Loading schema " + schema + " at url " + url); 1131 Class.forName(className); 1132 retval = DriverManager.getConnection(url, username, password); 1133 retval.setAutoCommit(true); 1134 } 1135 catch (Exception e) { 1136 if (!e.getMessage().contains("Database lock acquisition failure") && !(e instanceof NullPointerException)) { 1137 throw new MojoExecutionException(e.getMessage(), e); 1138 } 1139 } 1140 finally { 1141 retry_count++; 1142 } 1143 } 1144 return new JdbcConnection(retval); 1145 } 1146 1147 @Override 1148 protected ResourceAccessor getFileOpener(final ClassLoader cl) { 1149 final ResourceAccessor mFO = new MavenResourceAccessor(cl); 1150 final ResourceAccessor fsFO = new FileSystemResourceAccessor(project.getBasedir().getAbsolutePath()); 1151 return new CompositeResourceAccessor(mFO, fsFO); 1152 } 1153 1154 public void setMavenHome(final File mavenHome) { 1155 this.mavenHome = mavenHome; 1156 } 1157 1158 public File getMavenHome() { 1159 return this.mavenHome; 1160 } 1161 1162}