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