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}