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}