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.project.MavenProject;
032import org.apache.maven.artifact.manager.WagonManager;
033
034import org.liquibase.maven.plugins.AbstractLiquibaseMojo;
035import org.liquibase.maven.plugins.AbstractLiquibaseUpdateMojo;
036import org.liquibase.maven.plugins.LiquibaseRollback;
037
038import liquibase.Liquibase;
039import liquibase.exception.LiquibaseException;
040import liquibase.serializer.ChangeLogSerializer;
041import liquibase.parser.core.xml.LiquibaseEntityResolver;
042import liquibase.parser.core.xml.XMLChangeLogSAXParser;
043import org.apache.maven.wagon.authentication.AuthenticationInfo;
044
045import liquibase.util.xml.DefaultXmlWriter;
046
047import org.tmatesoft.svn.core.ISVNDirEntryHandler;
048import org.tmatesoft.svn.core.SVNDirEntry;
049import org.tmatesoft.svn.core.SVNDepth;
050import org.tmatesoft.svn.core.SVNException;
051import org.tmatesoft.svn.core.SVNURL;
052import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
053import org.tmatesoft.svn.core.io.SVNRepository;
054import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
055import org.tmatesoft.svn.core.wc.ISVNOptions;
056import org.tmatesoft.svn.core.wc.SVNWCUtil;
057import org.tmatesoft.svn.core.wc.SVNClientManager;
058import org.tmatesoft.svn.core.wc.SVNWCClient;
059import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
060
061import org.w3c.dom.*;
062
063import javax.xml.parsers.DocumentBuilder;
064import javax.xml.parsers.DocumentBuilderFactory;
065import javax.xml.parsers.ParserConfigurationException;
066
067import java.lang.reflect.Field;
068import java.lang.reflect.Method;
069
070import java.io.File;
071import java.io.FileNotFoundException;
072import java.io.FileOutputStream;
073import java.io.InputStream;
074import java.io.IOException;
075import java.net.URL;
076import java.util.ArrayList;
077import java.util.Arrays;
078import java.util.Collection;
079import java.util.Iterator;
080import java.util.List;
081import java.util.Properties;
082
083import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD;
084import static org.tmatesoft.svn.core.wc.SVNRevision.WORKING;
085
086/**
087 * Undo for a migration of Liquibase changelogs. Basically, rolls back any changes that were made in the most recent
088 * migration.
089 *
090 * @author Leo Przybylski
091 * @goal rollback
092 */
093public class RollbackMojo extends LiquibaseRollback {
094    public static final String DEFAULT_CHANGELOG_PATH = "src/main/changelogs";
095
096    /**
097     * Suffix for fields that are representing a default value for a another field.
098     */
099    private static final String DEFAULT_FIELD_SUFFIX = "Default";
100
101    /**
102     * The fully qualified name of the driver class to use to connect to the database.
103     *
104     * @parameter expression="${liquibase.driver}"
105     */
106    protected String driver;
107
108    /**
109     * The Database URL to connect to for executing Liquibase.
110     *
111     * @parameter expression="${liquibase.url}"
112     */
113    protected String url;
114
115    /**
116
117     The Maven Wagon manager to use when obtaining server authentication details.
118     @component role="org.apache.maven.artifact.manager.WagonManager"
119     @required
120     @readonly
121     */
122    protected WagonManager wagonManager;
123    /**
124     * The server id in settings.xml to use when authenticating with.
125     *
126     * @parameter expression="${liquibase.server}"
127     */
128    private String server;
129
130    /**
131     * The database username to use to connect to the specified database.
132     *
133     * @parameter expression="${liquibase.username}"
134     */
135    protected String username;
136
137    /**
138     * The database password to use to connect to the specified database.
139     *
140     * @parameter expression="${liquibase.password}"
141     */
142    protected String password;
143
144    /**
145     * Use an empty string as the password for the database connection. This should not be
146     * used along side the {@link #password} setting.
147     *
148     * @parameter expression="${liquibase.emptyPassword}" default-value="false"
149     * @deprecated Use an empty or null value for the password instead.
150     */
151    protected boolean emptyPassword;
152
153    /**
154     * The default schema name to use the for database connection.
155     *
156     * @parameter expression="${liquibase.defaultSchemaName}"
157     */
158    protected String defaultSchemaName;
159
160    /**
161     * The class to use as the database object.
162     *
163     * @parameter expression="${liquibase.databaseClass}"
164     */
165    protected String databaseClass;
166
167    /**
168     * Controls the prompting of users as to whether or not they really want to run the
169     * changes on a database that is not local to the machine that the user is current
170     * executing the plugin on.
171     *
172     * @parameter expression="${liquibase.promptOnNonLocalDatabase}" default-value="true"
173     */
174    protected boolean promptOnNonLocalDatabase;
175
176    /**
177     * Allows for the maven project artifact to be included in the class loader for
178     * obtaining the Liquibase property and DatabaseChangeLog files.
179     *
180     * @parameter expression="${liquibase.includeArtifact}" default-value="true"
181     */
182    protected boolean includeArtifact;
183
184    /**
185     * Allows for the maven test output directory to be included in the class loader for
186     * obtaining the Liquibase property and DatabaseChangeLog files.
187     *
188     * @parameter expression="${liquibase.includeTestOutputDirectory}" default-value="true"
189     */
190    protected boolean includeTestOutputDirectory;
191
192    /**
193     * Controls the verbosity of the output from invoking the plugin.
194     *
195     * @parameter expression="${liquibase.verbose}" default-value="false"
196     * @description Controls the verbosity of the plugin when executing
197     */
198    protected boolean verbose;
199
200    /**
201     * Controls the level of logging from Liquibase when executing. The value can be
202     * "all", "finest", "finer", "fine", "info", "warning", "severe" or "off". The value is
203     * case insensitive.
204     *
205     * @parameter expression="${liquibase.logging}" default-value="INFO"
206     * @description Controls the verbosity of the plugin when executing
207     */
208    protected String logging;
209
210    /**
211     * The Liquibase properties file used to configure the Liquibase {@link
212     * liquibase.Liquibase}.
213     *
214     * @parameter expression="${liquibase.propertyFile}"
215     */
216    protected String propertyFile;
217
218    /**
219     * Flag allowing for the Liquibase properties file to override any settings provided in
220     * the Maven plugin configuration. By default if a property is explicity specified it is
221     * not overridden if it also appears in the properties file.
222     *
223     * @parameter expression="${liquibase.propertyFileWillOverride}" default-value="false"
224     */
225    protected boolean propertyFileWillOverride;
226
227    /**
228     * Flag for forcing the checksums to be cleared from teh DatabaseChangeLog table.
229     *
230     * @parameter expression="${liquibase.clearCheckSums}" default-value="false"
231     */
232    protected boolean clearCheckSums;
233
234    /**                                                                                                                                                                          
235     * List of system properties to pass to the database.                                                                                                                        
236     *                                                                                                                                                                           
237     * @parameter                                                                                                                                                                
238     */                                                                                                                                                                          
239    protected Properties systemProperties;
240
241    protected File getBasedir() {
242        return project.getBasedir();
243    }
244
245    protected void doFieldHack() {
246        for (final Field field : getClass().getDeclaredFields()) {
247            try {
248                final Field parentField = getDeclaredField(getClass().getSuperclass(), field.getName());
249                if (parentField != null) {
250                    getLog().debug("Setting " + field.getName() + " in " + parentField.getDeclaringClass().getName() + " to " + field.get(this));
251                    parentField.set(this, field.get(this));
252                }
253            }
254            catch (Exception e) {
255            }
256        }
257    }
258
259    @Override
260    public void execute() throws MojoExecutionException, MojoFailureException {
261        doFieldHack();
262
263        try {
264            Method meth = AbstractLiquibaseMojo.class.getDeclaredMethod("processSystemProperties");
265            meth.setAccessible(true);
266            meth.invoke(this);
267        }
268        catch (Exception e) {
269            e.printStackTrace();
270        }
271
272        ClassLoader artifactClassLoader = getMavenArtifactClassLoader();
273        configureFieldsAndValues(getFileOpener(artifactClassLoader));
274        
275        doFieldHack();
276
277        super.execute();
278    }
279    
280    @Override
281    protected void performLiquibaseTask(Liquibase liquibase) throws LiquibaseException {
282        liquibase.rollback("undo", contexts);
283    }
284      
285    
286    @Override
287    protected void printSettings(String indent) {
288        super.printSettings(indent);
289    }
290
291    /**
292     * Parses a properties file and sets the assocaited fields in the plugin.
293     *
294     * @param propertiesInputStream The input stream which is the Liquibase properties that
295     *                              needs to be parsed.
296     * @throws org.apache.maven.plugin.MojoExecutionException
297     *          If there is a problem parsing
298     *          the file.
299     */
300    protected void parsePropertiesFile(InputStream propertiesInputStream)
301            throws MojoExecutionException {
302        if (propertiesInputStream == null) {
303            throw new MojoExecutionException("Properties file InputStream is null.");
304        }
305        Properties props = new Properties();
306        try {
307            props.load(propertiesInputStream);
308        }
309        catch (IOException e) {
310            throw new MojoExecutionException("Could not load the properties Liquibase file", e);
311        }
312
313        for (Iterator it = props.keySet().iterator(); it.hasNext();) {
314            String key = null;
315            try {
316                key = (String) it.next();
317                Field field = getDeclaredField(this.getClass(), key);
318
319                if (propertyFileWillOverride) {
320                    setFieldValue(field, props.get(key).toString());
321                } else {
322                    if (!isCurrentFieldValueSpecified(field)) {
323                        getLog().debug("  properties file setting value: " + field.getName());
324                        setFieldValue(field, props.get(key).toString());
325                    }
326                }
327            }
328            catch (Exception e) {
329                getLog().info("  '" + key + "' in properties file is not being used by this "
330                        + "task.");
331            }
332        }
333    }
334
335    /**
336     * This method will check to see if the user has specified a value different to that of
337     * the default value. This is not an ideal solution, but should cover most situations in
338     * the use of the plugin.
339     *
340     * @param f The Field to check if a user has specified a value for.
341     * @return <code>true</code> if the user has specified a value.
342     */
343    private boolean isCurrentFieldValueSpecified(Field f) throws IllegalAccessException {
344        Object currentValue = f.get(this);
345        if (currentValue == null) {
346            return false;
347        }
348
349        Object defaultValue = getDefaultValue(f);
350        if (defaultValue == null) {
351            return currentValue != null;
352        } else {
353            // There is a default value, check to see if the user has selected something other
354            // than the default
355            return !defaultValue.equals(f.get(this));
356        }
357    }
358
359    private Object getDefaultValue(Field field) throws IllegalAccessException {
360        List<Field> allFields = new ArrayList<Field>();
361        allFields.addAll(Arrays.asList(getClass().getDeclaredFields()));
362        allFields.addAll(Arrays.asList(AbstractLiquibaseMojo.class.getDeclaredFields()));
363
364        for (Field f : allFields) {
365            if (f.getName().equals(field.getName() + DEFAULT_FIELD_SUFFIX)) {
366                f.setAccessible(true);
367                return f.get(this);
368            }
369        }
370        return null;
371    }
372
373    
374    /**
375     * Recursively searches for the field specified by the fieldName in the class and all
376     * the super classes until it either finds it, or runs out of parents.
377     * @param clazz The Class to start searching from.
378     * @param fieldName The name of the field to retrieve.
379     * @return The {@link Field} identified by the field name.
380     * @throws NoSuchFieldException If the field was not found in the class or any of its
381     * super classes.
382     */
383    protected Field getDeclaredField(Class clazz, String fieldName)
384        throws NoSuchFieldException {
385        getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'");
386        try {
387            Field f = clazz.getDeclaredField(fieldName);
388            
389            if (f != null) {
390                return f;
391            }
392        }
393        catch (Exception e) {
394        }
395        
396        while (clazz.getSuperclass() != null) {        
397            clazz = clazz.getSuperclass();
398            getLog().debug("Checking " + clazz.getName() + " for '" + fieldName + "'");
399            try {
400                Field f = clazz.getDeclaredField(fieldName);
401                
402                if (f != null) {
403                    return f;
404                }
405            }
406            catch (Exception e) {
407            }
408        }
409
410        throw new NoSuchFieldException("The field '" + fieldName + "' could not be "
411                                       + "found in the class of any of its parent "
412                                       + "classes.");
413    }
414
415    private void setFieldValue(Field field, String value) throws IllegalAccessException {
416        if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) {
417            field.set(this, Boolean.valueOf(value));
418        } else {
419            field.set(this, value);
420        }
421    }
422}