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}