001// Copyright 2014 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 Leo Przybylski ''AS IS'' AND ANY EXPRESS OR IMPLIED
014// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
015// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
016// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
017// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
018// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
019// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
020// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
021// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
022//
023// The views and conclusions contained in the software and documentation are those of the
024// authors and should not be interpreted as representing official policies, either expressed
025// or implied, of Leo Przybylski.
026package org.kualigan.maven.plugins.liquibase;
027
028import org.apache.maven.plugin.AbstractMojo;
029import org.apache.maven.plugin.MojoExecutionException;
030import org.apache.maven.plugin.MojoFailureException;
031import org.apache.maven.plugins.annotations.Component;
032import org.apache.maven.plugins.annotations.Mojo;
033import org.apache.maven.plugins.annotations.Parameter;
034import org.apache.maven.project.MavenProject;
035import org.apache.maven.artifact.manager.WagonManager;
036
037import org.liquibase.maven.plugins.AbstractLiquibaseMojo;
038import org.liquibase.maven.plugins.AbstractLiquibaseUpdateMojo;
039
040import liquibase.Liquibase;
041import liquibase.exception.LiquibaseException;
042import liquibase.serializer.ChangeLogSerializer;
043import liquibase.serializer.LiquibaseSerializable;
044import liquibase.parser.NamespaceDetails;
045import liquibase.parser.NamespaceDetailsFactory;
046import liquibase.parser.core.xml.LiquibaseEntityResolver;
047import liquibase.parser.core.xml.XMLChangeLogSAXParser;
048import liquibase.serializer.core.xml.XMLChangeLogSerializer;
049import org.apache.maven.wagon.authentication.AuthenticationInfo;
050
051import liquibase.util.xml.DefaultXmlWriter;
052
053import org.tmatesoft.svn.core.ISVNDirEntryHandler;
054import org.tmatesoft.svn.core.SVNDirEntry;
055import org.tmatesoft.svn.core.SVNDepth;
056import org.tmatesoft.svn.core.SVNException;
057import org.tmatesoft.svn.core.SVNURL;
058import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
059import org.tmatesoft.svn.core.io.SVNRepository;
060import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
061import org.tmatesoft.svn.core.wc.ISVNOptions;
062import org.tmatesoft.svn.core.wc.SVNWCUtil;
063import org.tmatesoft.svn.core.wc.SVNClientManager;
064import org.tmatesoft.svn.core.wc.SVNWCClient;
065import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
066
067import org.w3c.dom.*;
068
069import javax.xml.parsers.DocumentBuilder;
070import javax.xml.parsers.DocumentBuilderFactory;
071import javax.xml.parsers.ParserConfigurationException;
072
073import java.lang.reflect.Field;
074import java.lang.reflect.Method;
075
076import java.io.File;
077import java.io.FilenameFilter;
078import java.io.FileNotFoundException;
079import java.io.FileOutputStream;
080import java.io.InputStream;
081import java.io.IOException;
082import java.net.URL;
083import java.util.ArrayList;
084import java.util.Arrays;
085import java.util.Collection;
086import java.util.HashMap;
087import java.util.Iterator;
088import java.util.List;
089import java.util.Map;
090import java.util.Properties;
091
092import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD;
093import static org.tmatesoft.svn.core.wc.SVNRevision.WORKING;
094
095/**
096 * Migrate Liquibase changelogs
097 *
098 * @author Leo Przybylski
099 * @goal migrate
100 */
101public class MigrateMojo extends AbstractLiquibaseUpdateMojo {
102    public static final String DEFAULT_CHANGELOG_PATH = "src/main/scripts/changelogs";
103    public static final String DEFAULT_UPDATE_FILE    = "target/changelogs/update.xml";
104    public static final String DEFAULT_UPDATE_PATH    = "target/changelogs/update";
105    public static final String DEFAULT_LBPROP_PATH    = "target/test-classes/liquibase/";
106    public static final String TEST_ROLLBACK_TAG      = "test";
107
108    /**
109     * Suffix for fields that are representing a default value for a another field.
110     */
111    private static final String DEFAULT_FIELD_SUFFIX = "Default";
112
113    /**
114     * The fully qualified name of the driver class to use to connect to the database.
115     *
116     * @parameter expression="${liquibase.driver}"
117     */
118    protected String driver;
119
120    /**
121     * The Database URL to connect to for executing Liquibase.
122     *
123     * @parameter expression="${liquibase.url}"
124     */
125    protected String url;
126
127    /**
128
129       The Maven Wagon manager to use when obtaining server authentication details.
130       @component role="org.apache.maven.artifact.manager.WagonManager"
131       @required
132       @readonly
133    */
134    protected WagonManager wagonManager;
135    /**
136     * The server id in settings.xml to use when authenticating with.
137     *
138     * @parameter expression="${liquibase.server}"
139     */
140    private String server;
141
142    /**
143     * The database username to use to connect to the specified database.
144     *
145     * @parameter expression="${liquibase.username}"
146     */
147    protected String username;
148
149    /**
150     * The database password to use to connect to the specified database.
151     *
152     * @parameter expression="${liquibase.password}"
153     */
154    protected String password;
155
156    /**
157     * Use an empty string as the password for the database connection. This should not be
158     * used along side the {@link #password} setting.
159     *
160     * @parameter expression="${liquibase.emptyPassword}" default-value="false"
161     * @deprecated Use an empty or null value for the password instead.
162     */
163    protected boolean emptyPassword;
164
165    /**
166     * The default schema name to use the for database connection.
167     *
168     * @parameter expression="${liquibase.defaultSchemaName}"
169     */
170    protected String defaultSchemaName;
171
172    /**
173     * The class to use as the database object.
174     *
175     * @parameter expression="${liquibase.databaseClass}"
176     */
177    protected String databaseClass;
178
179    /**
180     * Controls the prompting of users as to whether or not they really want to run the
181     * changes on a database that is not local to the machine that the user is current
182     * executing the plugin on.
183     *
184     * @parameter expression="${liquibase.promptOnNonLocalDatabase}" default-value="true"
185     */
186    protected boolean promptOnNonLocalDatabase;
187
188    /**
189     * Allows for the maven project artifact to be included in the class loader for
190     * obtaining the Liquibase property and DatabaseChangeLog files.
191     *
192     * @parameter expression="${liquibase.includeArtifact}" default-value="true"
193     */
194    protected boolean includeArtifact;
195
196    /**
197     * Allows for the maven test output directory to be included in the class loader for
198     * obtaining the Liquibase property and DatabaseChangeLog files.
199     *
200     * @parameter expression="${liquibase.includeTestOutputDirectory}" default-value="true"
201     */
202    protected boolean includeTestOutputDirectory;
203
204    /**
205     * Controls the verbosity of the output from invoking the plugin.
206     *
207     * @parameter expression="${liquibase.verbose}" default-value="false"
208     * @description Controls the verbosity of the plugin when executing
209     */
210    protected boolean verbose;
211
212    /**
213     * Controls the level of logging from Liquibase when executing. The value can be
214     * "all", "finest", "finer", "fine", "info", "warning", "severe" or "off". The value is
215     * case insensitive.
216     *
217     * @parameter expression="${liquibase.logging}" default-value="INFO"
218     * @description Controls the verbosity of the plugin when executing
219     */
220    protected String logging;
221
222    /**
223     * The Liquibase properties file used to configure the Liquibase {@link
224     * liquibase.Liquibase}.
225     *
226     * @parameter expression="${liquibase.propertyFile}"
227     */
228    protected String propertyFile;
229
230    /**
231     * Flag allowing for the Liquibase properties file to override any settings provided in
232     * the Maven plugin configuration. By default if a property is explicity specified it is
233     * not overridden if it also appears in the properties file.
234     *
235     * @parameter expression="${liquibase.propertyFileWillOverride}" default-value="false"
236     */
237    protected boolean propertyFileWillOverride;
238
239    /**
240     * Flag for forcing the checksums to be cleared from teh DatabaseChangeLog table.
241     *
242     * @parameter expression="${liquibase.clearCheckSums}" default-value="false"
243     */
244    protected boolean clearCheckSums;
245
246    /**                                                                                                                                                                          
247     * List of system properties to pass to the database.                                                                                                                        
248     *                                                                                                                                                                           
249     * @parameter                                                                                                                                                                
250     */                                                                                                                                                                          
251    protected Properties systemProperties;
252
253    protected String svnUsername;
254    protected String svnPassword;
255
256    /**
257     * The server id in settings.xml to use when authenticating with.
258     *
259     * @parameter expression="${lb.svnServer}"
260     */
261    protected String svnServer;
262    
263    /**
264     * @parameter default-value="${project.basedir}/src/main/scripts/changelogs"
265     */
266    protected File changeLogSavePath;
267
268    /**
269     * @parameter expression="${lb.changeLogTagUrl}"
270     */
271    protected URL changeLogTagUrl;
272
273    /**
274     * Location of an update.xml
275     *
276     * @parameter expression="${lb.updatePath}" default-value="${project.basedir}/src/main/changelogs"
277     */
278    protected File updatePath;
279
280    /**
281     * The Maven project that plugin is running under.
282     * @parameter expression="${project}"
283     * @required
284     * @readonly
285     */
286    @Parameter(property = "project", required = true)
287    protected MavenProject project;
288
289    
290    /**
291     * Whether or not to perform a drop on the database before executing the change.
292     * @parameter expression="${liquibase.dropFirst}" default-value="false"
293     */
294    protected boolean dropFirst;
295
296    protected File getBasedir() {
297        return project.getBasedir();
298    }
299
300    protected SVNURL getChangeLogTagUrl() throws SVNException {
301        if (changeLogTagUrl == null) {
302            return getProjectSvnUrlFrom(getBasedir()).appendPath("tags", true);
303        }
304        return SVNURL.parseURIEncoded(changeLogTagUrl.toString());
305    }
306
307    protected void doFieldHack() {
308        for (final Field field : getClass().getDeclaredFields()) {
309            try {
310                final Field parentField = getDeclaredField(getClass().getSuperclass(), field.getName());
311                if (parentField != null) {
312                    getLog().debug("Setting " + field.getName() + " in " + parentField.getDeclaringClass().getName() + " to " + field.get(this));
313                    parentField.set(this, field.get(this));
314                }
315            }
316            catch (Exception e) {
317            }
318        }
319    }
320
321    protected File[] getLiquibasePropertiesFiles() throws MojoExecutionException {
322        try {
323            final File[] retval = new File(getBasedir(), DEFAULT_LBPROP_PATH).listFiles(new FilenameFilter() {
324                    public boolean accept(final File dir, final String name) {
325                        return name.endsWith(".properties");
326                    }
327                });
328            if (retval == null) {
329                throw new NullPointerException();
330            }            
331            return retval;
332        }
333        catch (Exception e) {
334            getLog().warn("Unable to get liquibase properties files ");
335            return new File[0];
336            // throw new MojoExecutionException("Unable to get liquibase properties files ", e);
337        }
338    }
339
340    @Override
341    public void execute() throws MojoExecutionException, MojoFailureException {
342        doFieldHack();
343
344        try {
345            Method meth = AbstractLiquibaseMojo.class.getDeclaredMethod("processSystemProperties");
346            meth.setAccessible(true);
347            meth.invoke(this);
348        }
349        catch (Exception e) {
350            e.printStackTrace();
351        }
352
353        ClassLoader artifactClassLoader = getMavenArtifactClassLoader();
354        final File[] propertyFiles = getLiquibasePropertiesFiles();
355        
356        // execute change logs on each database
357        for (final File props : propertyFiles) {
358            try {
359                propertyFile = props.getCanonicalPath();
360                doFieldHack();
361                
362                configureFieldsAndValues(getFileOpener(artifactClassLoader));
363                
364                doFieldHack();
365            }
366            catch (Exception e) {
367                throw new MojoExecutionException(e.getMessage(), e);
368            }
369        }
370
371        if (svnServer != null) {
372            final AuthenticationInfo info = wagonManager.getAuthenticationInfo(svnServer);
373            if (info != null) {
374                svnUsername = info.getUserName();
375                svnPassword = info.getPassword();
376            }
377        }
378        DAVRepositoryFactory.setup();
379
380        if (!isUpdateRequired()) {
381            return;
382        }
383
384
385        boolean shouldLocalUpdate = false;
386        try {
387            final Collection<SVNURL> svnurls = getTagUrls();
388            shouldLocalUpdate = (svnurls == null || svnurls.size() < 1);
389
390            for (final SVNURL tag : svnurls) {
391                final String tagBasePath = getLocalTagPath(tag);
392                
393                final File tagPath = new File(tagBasePath, "update");
394                tagPath.mkdirs();
395                
396                final SVNURL changeLogUrl = tag.appendPath(DEFAULT_CHANGELOG_PATH + "/update", true);
397                SVNClientManager.newInstance().getUpdateClient()
398                    .doExport(changeLogUrl, tagPath, HEAD, HEAD, null, true, SVNDepth.INFINITY);
399            }
400        }
401        catch (Exception e) {
402            throw new MojoExecutionException("Exception when exporting changelogs from previous revisions", e);
403        }
404
405        changeLogFile = new File(changeLogSavePath, "update.xml").getPath();
406        File changeLogSearchPath = changeLogSavePath;
407
408        if (shouldLocalUpdate) {
409            changeLogSavePath = new File(changeLogSavePath, "update");
410        }
411        
412        final Collection<File> changelogs = scanForChangelogs(changeLogSearchPath);
413        
414        try {
415            generateUpdateLog(new File(changeLogFile), changelogs);
416        }
417        catch (Exception e) {
418            throw new MojoExecutionException("Failed to generate changelog file " + changeLogFile, e);
419        }
420        
421        super.execute();
422    }
423
424    protected String getLocalTagPath(final SVNURL tag) {
425        final String tagPath = tag.getPath();
426        return changeLogSavePath + File.separator + tagPath.substring(tagPath.lastIndexOf("/") + 1);
427    }
428
429    protected boolean isUpdateRequired() throws MojoExecutionException {
430        try {
431            getLog().debug("Comparing " + getCurrentRevision() + " to " + getLocalRevision());
432            final String[] updates = new File(DEFAULT_CHANGELOG_PATH + File.separator + "update").list();
433            boolean hasUpdates = updates != null && updates.length > 0;
434            return getCurrentRevision() > getLocalRevision() || (hasUpdates);
435        }
436        catch (Exception e) {
437            throw new MojoExecutionException("Could not compare local and remote revisions ", e);
438        }
439    }
440
441    protected SVNURL getProjectSvnUrlFrom(final File path) throws SVNException {
442        SVNURL retval = getWCClient().doInfo(getBasedir(), HEAD).getURL();
443        String removeToken = null;
444        if (retval.getPath().indexOf("/branches") > -1) {
445            removeToken = "/branches";
446        }
447        else if (retval.getPath().indexOf("/tags") > -1) {
448            removeToken = "/tags";
449        }
450        else if (retval.getPath().indexOf("/trunk") > -1) {
451            removeToken = "/trunk";
452        }
453
454        getLog().debug("Checking path " + retval.getPath() + " for token " + removeToken);
455        while (retval.getPath().indexOf(removeToken) > -1) {
456            retval = retval.removePathTail();
457        }
458        return retval;
459    }
460
461    protected Long getCurrentRevision() throws SVNException {
462        return getWCClient().doInfo(getBasedir(), HEAD).getCommittedRevision().getNumber();
463    }
464
465    protected Long getLocalRevision() throws SVNException {
466        return getWCClient().doInfo(getBasedir(), WORKING).getRevision().getNumber();
467    }
468
469    protected Long getTagRevision(final String tag) throws SVNException {
470        return getWCClient().doInfo(getChangeLogTagUrl(), WORKING, WORKING).getRevision().getNumber();
471    }
472
473    protected Collection<SVNURL> getTagUrls() throws SVNException {
474        final Collection<SVNURL> retval = new ArrayList<SVNURL>();
475        getLog().debug("Looking up tags in " + getChangeLogTagUrl().toString());
476        clientManager().getLogClient()
477            .doList(getChangeLogTagUrl(), HEAD, HEAD, false, false, 
478                    new ISVNDirEntryHandler() {
479                        public void handleDirEntry(SVNDirEntry dirEntry) throws SVNException {
480                            if (dirEntry.getRevision() >= getLocalRevision()
481                                && dirEntry.getPath().trim().length() > 0) {
482                                getLog().debug("Adding tag '" + dirEntry.getPath() + "'");
483                                retval.add(dirEntry.getURL());
484                            }
485                        }
486                    });
487        return retval;
488    }
489
490    protected SVNWCClient getWCClient() {
491        return clientManager().getWCClient();
492    }
493
494    protected Collection<File> scanForChangelogs(final File searchPath) {
495        final Collection<File> retval = new ArrayList<File>();
496        
497        if (searchPath.getName().endsWith("update")) {
498            return Arrays.asList(searchPath.listFiles());
499        }
500        
501        if (searchPath.isDirectory()) {
502            for (final File file : searchPath.listFiles()) {
503                if (file.isDirectory()) {
504                    retval.addAll(scanForChangelogs(file));
505                }
506            }
507        }
508        
509        return retval;
510    }
511
512    protected SVNClientManager clientManager() {
513        final ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager("lprzybylski", "entr0py0");
514        final ISVNOptions options = SVNWCUtil.createDefaultOptions(true);       
515        final SVNClientManager clientManager = SVNClientManager.newInstance(options, authManager);
516        
517        return clientManager;
518    }
519
520    protected void generateUpdateLog(final File changeLogFile, 
521                                     final Collection<File> changelogs) throws FileNotFoundException, IOException {
522        changeLogFile.getParentFile().mkdirs();
523
524        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
525        DocumentBuilder documentBuilder;
526        try {
527            documentBuilder = factory.newDocumentBuilder();
528        }
529        catch (ParserConfigurationException e) {
530            throw new RuntimeException(e);
531        }
532        final XMLChangeLogSerializer serializer = new XMLChangeLogSerializer();
533        documentBuilder.setEntityResolver(new LiquibaseEntityResolver(serializer));
534
535        final Document doc = documentBuilder.newDocument();
536        final Element changeLogElement = doc.createElementNS(LiquibaseSerializable.STANDARD_CHANGELOG_NAMESPACE, "databaseChangeLog");
537
538        changeLogElement.setAttribute("xmlns", LiquibaseSerializable.STANDARD_CHANGELOG_NAMESPACE);
539        changeLogElement.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
540
541        final Map<String, String> shortNameByNamespace = new HashMap<String, String>();
542        final Map<String, String> urlByNamespace = new HashMap<String, String>();
543
544        for (final NamespaceDetails details : NamespaceDetailsFactory.getInstance().getNamespaceDetails()) {
545            for (final String namespace : details.getNamespaces()) {
546                if (details.supports(serializer, namespace)){
547                    final String shortName = details.getShortName(namespace);
548                    final String url = details.getSchemaUrl(namespace);
549                    if (shortName != null && url != null) {
550                        shortNameByNamespace.put(namespace, shortName);
551                        urlByNamespace.put(namespace, url);
552                    }
553                }
554            }
555        }
556
557        for (final Map.Entry<String, String> entry : shortNameByNamespace.entrySet()) {
558            if (!entry.getValue().equals("")) {
559                changeLogElement.setAttribute("xmlns:"+entry.getValue(), entry.getKey());
560            }
561        }
562
563
564        String schemaLocationAttribute = "";
565        for (final Map.Entry<String, String> entry : urlByNamespace.entrySet()) {
566            if (!entry.getValue().equals("")) {
567                schemaLocationAttribute += entry.getKey()+" "+entry.getValue()+" ";
568            }
569        }
570
571        changeLogElement.setAttribute("xsi:schemaLocation", schemaLocationAttribute.trim());
572
573        doc.appendChild(changeLogElement);
574
575        for (final File changelog : changelogs) {
576            doc.getDocumentElement().appendChild(includeNode(doc, changelog));
577        }
578
579        new DefaultXmlWriter().write(doc, new FileOutputStream(changeLogFile));
580    }
581
582    protected Element includeNode(final Document parentChangeLog, final File changelog) throws IOException {
583        final Element retval = parentChangeLog.createElementNS(LiquibaseSerializable.STANDARD_CHANGELOG_NAMESPACE, 
584                                                               "databaseChangeLog");
585
586        retval.setAttribute("file", changelog.getCanonicalPath());
587        return retval;
588    }
589
590    @Override
591    protected void doUpdate(final Liquibase liquibase) throws LiquibaseException {
592        if (dropFirst) {
593            dropAll(liquibase);
594        }
595
596        liquibase.tag("undo");
597
598        if (changesToApply > 0) {
599            liquibase.update(changesToApply, contexts);
600        } else {
601            liquibase.update(contexts);
602        }
603    }
604
605    /**
606     * Drops the database. Makes sure it's done right the first time.
607     *
608     * @param liquibase
609     * @throws LiquibaseException
610     */
611    protected void dropAll(final Liquibase liquibase) throws LiquibaseException {
612        boolean retry = true;
613        while (retry) {
614            try {
615                liquibase.dropAll();
616                retry = false;
617            }
618            catch (LiquibaseException e2) {
619                getLog().info(e2.getMessage());
620                if (e2.getMessage().indexOf("ORA-02443") < 0 && e2.getCause() != null && retry) {
621                    retry = (e2.getCause().getMessage().indexOf("ORA-02443") > -1);
622                }
623                
624                if (!retry) {
625                    throw e2;
626                }
627                else {
628                    getLog().info("Got ORA-2443. Retrying...");
629                }
630            }
631        }        
632    }
633    
634    @Override
635    protected void printSettings(String indent) {
636        super.printSettings(indent);
637        getLog().info(indent + "drop first? " + dropFirst);
638
639    }
640
641    /**
642     * Parses a properties file and sets the assocaited fields in the plugin.
643     *
644     * @param propertiesInputStream The input stream which is the Liquibase properties that
645     *                              needs to be parsed.
646     * @throws org.apache.maven.plugin.MojoExecutionException
647     *          If there is a problem parsing
648     *          the file.
649     */
650    protected void parsePropertiesFile(InputStream propertiesInputStream)
651        throws MojoExecutionException {
652        if (propertiesInputStream == null) {
653            throw new MojoExecutionException("Properties file InputStream is null.");
654        }
655        Properties props = new Properties();
656        try {
657            props.load(propertiesInputStream);
658        }
659        catch (IOException e) {
660            throw new MojoExecutionException("Could not load the properties Liquibase file", e);
661        }
662
663        for (Iterator it = props.keySet().iterator(); it.hasNext();) {
664            String key = null;
665            try {
666                key = (String) it.next();
667                Field field = getDeclaredField(this.getClass(), key);
668
669                if (propertyFileWillOverride) {
670                    setFieldValue(field, props.get(key).toString());
671                } else {
672                    if (!isCurrentFieldValueSpecified(field)) {
673                        getLog().debug("  properties file setting value: " + field.getName());
674                        setFieldValue(field, props.get(key).toString());
675                    }
676                }
677            }
678            catch (Exception e) {
679                getLog().info("  '" + key + "' in properties file is not being used by this "
680                              + "task.");
681            }
682        }
683    }
684
685    /**
686     * This method will check to see if the user has specified a value different to that of
687     * the default value. This is not an ideal solution, but should cover most situations in
688     * the use of the plugin.
689     *
690     * @param f The Field to check if a user has specified a value for.
691     * @return <code>true</code> if the user has specified a value.
692     */
693    private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException {
694        Object currentValue = f.get(this);
695        if (currentValue == null) {
696            return false;
697        }
698
699        Object defaultValue = getDefaultValue(f);
700        if (defaultValue == null) {
701            return currentValue != null;
702        } else {
703            // There is a default value, check to see if the user has selected something other
704            // than the default
705            return !defaultValue.equals(f.get(this));
706        }
707    }
708
709    private Object getDefaultValue(Field field) throws IllegalAccessException {
710        List<Field> allFields = new ArrayList<Field>();
711        allFields.addAll(Arrays.asList(getClass().getDeclaredFields()));
712        allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields()));
713
714        for (Field f : allFields) {
715            if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) {
716                f.setAccessible(true);
717                return f.get(this);
718            }
719        }
720        return null;
721    }
722
723    
724    /**
725     * Recursively searches for the field specified by the fieldName in the class and all
726     * the super classes until it either finds it, or runs out of parents.
727     * @param clazz The Class to start searching from.
728     * @param fieldName The name of the field to retrieve.
729     * @return The {@link Field} identified by the field name.
730     * @throws NoSuchFieldException If the field was not found in the class or any of its
731     * super classes.
732     */
733    protected Field getDeclaredField(Class clazz, String fieldName)
734        throws NoSuchFieldException {
735        getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'");
736        try {
737            Field f = clazz.getDeclaredField(fieldName);
738            
739            if (f != null) {
740                return f;
741            }
742        }
743        catch (Exception e) {
744        }
745        
746        while (clazz.getSuperclass() != null) {        
747            clazz = clazz.getSuperclass();
748            getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'");
749            try {
750                Field f = clazz.getDeclaredField(fieldName);
751                
752                if (f != null) {
753                    return f;
754                }
755            }
756            catch (Exception e) {
757            }
758        }
759
760        throw new NoSuchFieldException("The field '" + fieldName + "' could not be "
761                                       + "found in the class of any of its parent "
762                                       + "classes.");
763    }
764
765    private void setFieldValue(Field field, String value) throws IllegalAccessException {
766        if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) {
767            field.set(this, Boolean.valueOf(value));
768        } else {
769            field.set(this, value);
770        }
771    }
772}