001// Copyright 2011 Leo Przybylski. All rights reserved.
002//
003// Redistribution and use in source and binary forms, with or without modification, are
004// permitted provided that the following conditions are met:
005//
006//    1. Redistributions of source code must retain the above copyright notice, this list of
007//       conditions and the following disclaimer.
008//
009//    2. Redistributions in binary form must reproduce the above copyright notice, this list
010//       of conditions and the following disclaimer in the documentation and/or other materials
011//       provided with the distribution.
012//
013// THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED
014// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
015// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
016// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
017// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
018// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
019// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
020// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
021// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
022//
023// The views and conclusions contained in the software and documentation are those of the
024// authors and should not be interpreted as representing official policies, either expressed
025// or implied, of Leo Przybylski.
026package org.kualigan.maven.plugins.liquibase;
027
028import org.apache.maven.plugin.AbstractMojo;
029import org.apache.maven.plugin.MojoExecutionException;
030import org.apache.maven.plugin.MojoFailureException;
031import org.apache.maven.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;
039import org.liquibase.maven.plugins.LiquibaseRollback;
040
041import liquibase.Liquibase;
042import liquibase.exception.LiquibaseException;
043import liquibase.serializer.ChangeLogSerializer;
044import liquibase.parser.core.xml.LiquibaseEntityResolver;
045import liquibase.parser.core.xml.XMLChangeLogSAXParser;
046import org.apache.maven.wagon.authentication.AuthenticationInfo;
047
048import liquibase.util.xml.DefaultXmlWriter;
049
050import org.tmatesoft.svn.core.ISVNDirEntryHandler;
051import org.tmatesoft.svn.core.SVNDirEntry;
052import org.tmatesoft.svn.core.SVNDepth;
053import org.tmatesoft.svn.core.SVNException;
054import org.tmatesoft.svn.core.SVNURL;
055import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
056import org.tmatesoft.svn.core.io.SVNRepository;
057import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
058import org.tmatesoft.svn.core.wc.ISVNOptions;
059import org.tmatesoft.svn.core.wc.SVNWCUtil;
060import org.tmatesoft.svn.core.wc.SVNClientManager;
061import org.tmatesoft.svn.core.wc.SVNWCClient;
062import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
063
064import org.w3c.dom.*;
065
066import javax.xml.parsers.DocumentBuilder;
067import javax.xml.parsers.DocumentBuilderFactory;
068import javax.xml.parsers.ParserConfigurationException;
069
070import java.lang.reflect.Field;
071import java.lang.reflect.Method;
072
073import java.io.File;
074import java.io.FilenameFilter;
075import java.io.FileNotFoundException;
076import java.io.FileOutputStream;
077import java.io.InputStream;
078import java.io.IOException;
079import java.net.URL;
080import java.util.ArrayList;
081import java.util.Arrays;
082import java.util.Collection;
083import java.util.Iterator;
084import java.util.List;
085import java.util.Properties;
086
087import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD;
088import static org.tmatesoft.svn.core.wc.SVNRevision.WORKING;
089
090/**
091 * Undo for a migration of Liquibase changelogs. Basically, rolls back any changes that were made in the most recent
092 * migration.
093 *
094 * @author Leo Przybylski
095 * @goal rollback
096 */
097public class RollbackMojo extends LiquibaseRollback {
098    public static final String DEFAULT_CHANGELOG_PATH = "src/main/scripts/changelogs";
099    public static final String DEFAULT_UPDATE_FILE    = "target/changelogs/update.xml";
100    public static final String DEFAULT_UPDATE_PATH    = "target/changelogs/update";
101    public static final String DEFAULT_LBPROP_PATH    = "target/test-classes/liquibase/";
102    public static final String TEST_ROLLBACK_TAG      = "test";
103
104    /**
105     * Suffix for fields that are representing a default value for a another field.
106     */
107    private static final String DEFAULT_FIELD_SUFFIX = "Default";
108
109    /**
110     * The fully qualified name of the driver class to use to connect to the database.
111     *
112     * @parameter expression="${liquibase.driver}"
113     */
114    protected String driver;
115
116    /**
117     * The Database URL to connect to for executing Liquibase.
118     *
119     * @parameter expression="${liquibase.url}"
120     */
121    protected String url;
122
123    /**
124
125     The Maven Wagon manager to use when obtaining server authentication details.
126     @component role="org.apache.maven.artifact.manager.WagonManager"
127     @required
128     @readonly
129     */
130    protected WagonManager wagonManager;
131    /**
132     * The server id in settings.xml to use when authenticating with.
133     *
134     * @parameter expression="${liquibase.server}"
135     */
136    private String server;
137
138    /**
139     * The database username to use to connect to the specified database.
140     *
141     * @parameter expression="${liquibase.username}"
142     */
143    protected String username;
144
145    /**
146     * The database password to use to connect to the specified database.
147     *
148     * @parameter expression="${liquibase.password}"
149     */
150    protected String password;
151
152    /**
153     * Use an empty string as the password for the database connection. This should not be
154     * used along side the {@link #password} setting.
155     *
156     * @parameter expression="${liquibase.emptyPassword}" default-value="false"
157     * @deprecated Use an empty or null value for the password instead.
158     */
159    protected boolean emptyPassword;
160
161    /**
162     * The default schema name to use the for database connection.
163     *
164     * @parameter expression="${liquibase.defaultSchemaName}"
165     */
166    protected String defaultSchemaName;
167
168    /**
169     * The class to use as the database object.
170     *
171     * @parameter expression="${liquibase.databaseClass}"
172     */
173    protected String databaseClass;
174
175    /**
176     * Controls the prompting of users as to whether or not they really want to run the
177     * changes on a database that is not local to the machine that the user is current
178     * executing the plugin on.
179     *
180     * @parameter expression="${liquibase.promptOnNonLocalDatabase}" default-value="true"
181     */
182    protected boolean promptOnNonLocalDatabase;
183
184    /**
185     * Allows for the maven project artifact to be included in the class loader for
186     * obtaining the Liquibase property and DatabaseChangeLog files.
187     *
188     * @parameter expression="${liquibase.includeArtifact}" default-value="true"
189     */
190    protected boolean includeArtifact;
191
192    /**
193     * Allows for the maven test output directory to be included in the class loader for
194     * obtaining the Liquibase property and DatabaseChangeLog files.
195     *
196     * @parameter expression="${liquibase.includeTestOutputDirectory}" default-value="true"
197     */
198    protected boolean includeTestOutputDirectory;
199
200    /**
201     * Controls the verbosity of the output from invoking the plugin.
202     *
203     * @parameter expression="${liquibase.verbose}" default-value="false"
204     * @description Controls the verbosity of the plugin when executing
205     */
206    protected boolean verbose;
207
208    /**
209     * Controls the level of logging from Liquibase when executing. The value can be
210     * "all", "finest", "finer", "fine", "info", "warning", "severe" or "off". The value is
211     * case insensitive.
212     *
213     * @parameter expression="${liquibase.logging}" default-value="INFO"
214     * @description Controls the verbosity of the plugin when executing
215     */
216    protected String logging;
217
218    /**
219     * The Liquibase properties file used to configure the Liquibase {@link
220     * liquibase.Liquibase}.
221     *
222     * @parameter expression="${liquibase.propertyFile}"
223     */
224    protected String propertyFile;
225
226    /**
227     * Flag allowing for the Liquibase properties file to override any settings provided in
228     * the Maven plugin configuration. By default if a property is explicity specified it is
229     * not overridden if it also appears in the properties file.
230     *
231     * @parameter expression="${liquibase.propertyFileWillOverride}" default-value="false"
232     */
233    protected boolean propertyFileWillOverride;
234
235    /**
236     * Flag for forcing the checksums to be cleared from teh DatabaseChangeLog table.
237     *
238     * @parameter expression="${liquibase.clearCheckSums}" default-value="false"
239     */
240    protected boolean clearCheckSums;
241
242    /**                                                                                                                                                                          
243     * List of system properties to pass to the database.                                                                                                                        
244     *                                                                                                                                                                           
245     * @parameter                                                                                                                                                                
246     */                                                                                                                                                                          
247    protected Properties systemProperties;
248
249    /**
250     * @parameter default-value="${project.basedir}/src/main/scripts/changelogs"
251     */
252    protected File changeLogSavePath;
253
254    /**
255     * @parameter expression="${lb.changeLogTagUrl}"
256     */
257    protected URL changeLogTagUrl;
258
259    /**
260     * Location of an update.xml
261     *
262     * @parameter expression="${lb.updatePath}" default-value="${project.basedir}/src/main/changelogs"
263     */
264    protected File updatePath;
265
266    /**
267     * The Maven project that plugin is running under.
268     * @parameter expression="${project}"
269     * @required
270     * @readonly
271     */
272    @Parameter(property = "project", required = true)
273    protected MavenProject project;
274
275    protected File getBasedir() {
276        return project.getBasedir();
277    }
278
279    protected void doFieldHack() {
280        for (final Field field : getClass().getDeclaredFields()) {
281            try {
282                final Field parentField = getDeclaredField(getClass().getSuperclass(), field.getName());
283                if (parentField != null) {
284                    getLog().debug("Setting " + field.getName() + " in " + parentField.getDeclaringClass().getName() + " to " + field.get(this));
285                    parentField.set(this, field.get(this));
286                }
287            }
288            catch (Exception e) {
289            }
290        }
291    }
292
293    protected File[] getLiquibasePropertiesFiles() throws MojoExecutionException {
294        try {
295            final File[] retval = new File(getBasedir(), DEFAULT_LBPROP_PATH).listFiles(new FilenameFilter() {
296                    public boolean accept(final File dir, final String name) {
297                        return name.endsWith(".properties");
298                    }
299                });
300            if (retval == null) {
301                throw new NullPointerException();
302            }            
303            return retval;
304        }
305        catch (Exception e) {
306            getLog().warn("Unable to get liquibase properties files ");
307            return new File[0];
308            // throw new MojoExecutionException("Unable to get liquibase properties files ", e);
309        }
310    }
311
312    @Override
313    public void execute() throws MojoExecutionException, MojoFailureException {
314        doFieldHack();
315
316        try {
317            Method meth = AbstractLiquibaseMojo.class.getDeclaredMethod("processSystemProperties");
318            meth.setAccessible(true);
319            meth.invoke(this);
320        }
321        catch (Exception e) {
322            e.printStackTrace();
323        }
324
325        ClassLoader artifactClassLoader = getMavenArtifactClassLoader();
326        final File[] propertyFiles = getLiquibasePropertiesFiles();
327        
328        // execute change logs on each database
329        for (final File props : propertyFiles) {
330            try {
331                propertyFile = props.getCanonicalPath();
332                doFieldHack();
333                
334                configureFieldsAndValues(getFileOpener(artifactClassLoader));
335                
336                doFieldHack();
337            }
338            catch (Exception e) {
339                throw new MojoExecutionException(e.getMessage(), e);
340            }
341        }
342
343        changeLogFile = new File(changeLogSavePath, "update.xml").getPath();
344        rollbackCount = 1;
345
346        super.execute();
347    }
348    
349    @Override
350    protected void performLiquibaseTask(Liquibase liquibase) throws LiquibaseException {
351        liquibase.rollback("undo", contexts);
352    }
353      
354    
355    @Override
356    protected void printSettings(String indent) {
357        super.printSettings(indent);
358    }
359
360    /**
361     * Parses a properties file and sets the assocaited fields in the plugin.
362     *
363     * @param propertiesInputStream The input stream which is the Liquibase properties that
364     *                              needs to be parsed.
365     * @throws org.apache.maven.plugin.MojoExecutionException
366     *          If there is a problem parsing
367     *          the file.
368     */
369    protected void parsePropertiesFile(InputStream propertiesInputStream)
370            throws MojoExecutionException {
371        if (propertiesInputStream == null) {
372            throw new MojoExecutionException("Properties file InputStream is null.");
373        }
374        Properties props = new Properties();
375        try {
376            props.load(propertiesInputStream);
377        }
378        catch (IOException e) {
379            throw new MojoExecutionException("Could not load the properties Liquibase file", e);
380        }
381
382        for (Iterator it = props.keySet().iterator(); it.hasNext();) {
383            String key = null;
384            try {
385                key = (String) it.next();
386                Field field = getDeclaredField(this.getClass(), key);
387
388                if (propertyFileWillOverride) {
389                    setFieldValue(field, props.get(key).toString());
390                } else {
391                    if (!isCurrentFieldValueSpecified(field)) {
392                        getLog().debug("  properties file setting value: " + field.getName());
393                        setFieldValue(field, props.get(key).toString());
394                    }
395                }
396            }
397            catch (Exception e) {
398                getLog().info("  '" + key + "' in properties file is not being used by this "
399                        + "task.");
400            }
401        }
402    }
403
404    /**
405     * This method will check to see if the user has specified a value different to that of
406     * the default value. This is not an ideal solution, but should cover most situations in
407     * the use of the plugin.
408     *
409     * @param f The Field to check if a user has specified a value for.
410     * @return <code>true</code> if the user has specified a value.
411     */
412    private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException {
413        Object currentValue = f.get(this);
414        if (currentValue == null) {
415            return false;
416        }
417
418        Object defaultValue = getDefaultValue(f);
419        if (defaultValue == null) {
420            return currentValue != null;
421        } else {
422            // There is a default value, check to see if the user has selected something other
423            // than the default
424            return !defaultValue.equals(f.get(this));
425        }
426    }
427
428    private Object getDefaultValue(Field field) throws IllegalAccessException {
429        List<Field> allFields = new ArrayList<Field>();
430        allFields.addAll(Arrays.asList(getClass().getDeclaredFields()));
431        allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields()));
432
433        for (Field f : allFields) {
434            if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) {
435                f.setAccessible(true);
436                return f.get(this);
437            }
438        }
439        return null;
440    }
441
442    
443    /**
444     * Recursively searches for the field specified by the fieldName in the class and all
445     * the super classes until it either finds it, or runs out of parents.
446     * @param clazz The Class to start searching from.
447     * @param fieldName The name of the field to retrieve.
448     * @return The {@link Field} identified by the field name.
449     * @throws NoSuchFieldException If the field was not found in the class or any of its
450     * super classes.
451     */
452    protected Field getDeclaredField(Class clazz, String fieldName)
453        throws NoSuchFieldException {
454        getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'");
455        try {
456            Field f = clazz.getDeclaredField(fieldName);
457            
458            if (f != null) {
459                return f;
460            }
461        }
462        catch (Exception e) {
463        }
464        
465        while (clazz.getSuperclass() != null) {        
466            clazz = clazz.getSuperclass();
467            getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'");
468            try {
469                Field f = clazz.getDeclaredField(fieldName);
470                
471                if (f != null) {
472                    return f;
473                }
474            }
475            catch (Exception e) {
476            }
477        }
478
479        throw new NoSuchFieldException("The field '" + fieldName + "' could not be "
480                                       + "found in the class of any of its parent "
481                                       + "classes.");
482    }
483
484    private void setFieldValue(Field field, String value) throws IllegalAccessException {
485        if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) {
486            field.set(this, Boolean.valueOf(value));
487        } else {
488            field.set(this, value);
489        }
490    }
491}