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