    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.configuration2.io;
    018
    019import java.io.Closeable;
    020import java.io.File;
    021import java.io.IOException;
    022import java.io.InputStream;
    023import java.io.InputStreamReader;
    024import java.io.OutputStream;
    025import java.io.OutputStreamWriter;
    026import java.io.Reader;
    027import java.io.UnsupportedEncodingException;
    028import java.io.Writer;
    029import java.net.MalformedURLException;
    030import java.net.URL;
    031import java.util.List;
    032import java.util.Map;
    033import java.util.concurrent.CopyOnWriteArrayList;
    034import java.util.concurrent.atomic.AtomicReference;
    035
    036import org.apache.commons.configuration2.ex.ConfigurationException;
    037import org.apache.commons.configuration2.io.FileLocator.FileLocatorBuilder;
    038import org.apache.commons.configuration2.sync.LockMode;
    039import org.apache.commons.configuration2.sync.NoOpSynchronizer;
    040import org.apache.commons.configuration2.sync.Synchronizer;
    041import org.apache.commons.configuration2.sync.SynchronizerSupport;
    042import org.apache.commons.logging.LogFactory;
    043
    044/**
    045 * <p>
    046 * A class that manages persistence of an associated {@link FileBased} object.
    047 * </p>
    048 * <p>
    049 * Instances of this class can be used to load and save arbitrary objects
    050 * implementing the {@code FileBased} interface in a convenient way from and to
    051 * various locations. At construction time the {@code FileBased} object to
    052 * manage is passed in. Basically, this object is assigned a location from which
    053 * it is loaded and to which it can be saved. The following possibilities exist
    054 * to specify such a location:
    055 * </p>
    056 * <ul>
    057 * <li>URLs: With the method {@code setURL()} a full URL to the configuration
    058 * source can be specified. This is the most flexible way. Note that the
    059 * {@code save()} methods support only <em>file:</em> URLs.</li>
    060 * <li>Files: The {@code setFile()} method allows to specify the configuration
    061 * source as a file. This can be either a relative or an absolute file. In the
    062 * former case the file is resolved based on the current directory.</li>
    063 * <li>As file paths in string form: With the {@code setPath()} method a full
    064 * path to a configuration file can be provided as a string.</li>
    065 * <li>Separated as base path and file name: The base path is a string defining
    066 * either a local directory or a URL. It can be set using the
    067 * {@code setBasePath()} method. The file name, non surprisingly, defines the
    068 * name of the configuration file.</li>
    069 * </ul>
    070 * <p>
    071 * An instance stores a location. The {@code load()} and {@code save()} methods
    072 * that do not take an argument make use of this internal location.
    073 * Alternatively, it is also possible to use overloaded variants of
    074 * {@code load()} and {@code save()} which expect a location. In these cases the
    075 * location specified takes precedence over the internal one; the internal
    076 * location is not changed.
    077 * </p>
    078 * <p>
    079 * The actual position of the file to be loaded is determined by a
    080 * {@link FileLocationStrategy} based on the location information that has been
    081 * provided. By providing a custom location strategy the algorithm for searching
    082 * files can be adapted. Save operations require more explicit information. They
    083 * cannot rely on a location strategy because the file to be written may not yet
    084 * exist. So there may be some differences in the way location information is
    085 * interpreted by load and save operations. In order to avoid this, the
    086 * following approach is recommended:
    087 * </p>
    088 * <ul>
    089 * <li>Use the desired {@code setXXX()} methods to define the location of the
    090 * file to be loaded.</li>
    091 * <li>Call the {@code locate()} method. This method resolves the referenced
    092 * file (if possible) and fills out all supported location information.</li>
    093 * <li>Later on, {@code save()} can be called. This method now has sufficient
    094 * information to store the file at the correct location.</li>
    095 * </ul>
    096 * <p>
    097 * When loading or saving a {@code FileBased} object some additional
    098 * functionality is performed if the object implements one of the following
    099 * interfaces:
    100 * </p>
    101 * <ul>
    102 * <li>{@code FileLocatorAware}: In this case an object with the current file
    103 * location is injected before the load or save operation is executed. This is
    104 * useful for {@code FileBased} objects that depend on their current location,
    105 * e.g. to resolve relative path names.</li>
    106 * <li>{@code SynchronizerSupport}: If this interface is implemented, load and
    107 * save operations obtain a write lock on the {@code FileBased} object before
    108 * they access it. (In case of a save operation, a read lock would probably be
    109 * sufficient, but because of the possible injection of a {@link FileLocator}
    110 * object it is not allowed to perform multiple save operations in parallel;
    111 * therefore, by obtaining a write lock, we are on the safe side.)</li>
    112 * </ul>
    113 * <p>
    114 * This class is thread-safe.
    115 * </p>
    116 *
    117 * @since 2.0
    118 */
    119public class FileHandler
    120{
    121    /** Constant for the URI scheme for files. */
    122    private static final String FILE_SCHEME = "file:";
    123
    124    /** Constant for the URI scheme for files with slashes. */
    125    private static final String FILE_SCHEME_SLASH = FILE_SCHEME + "//";
    126
    127    /**
    128     * A dummy implementation of {@code SynchronizerSupport}. This object is
    129     * used when the file handler's content does not implement the
    130     * {@code SynchronizerSupport} interface. All methods are just empty dummy
    131     * implementations.
    132     */
    133    private static final SynchronizerSupport DUMMY_SYNC_SUPPORT =
    134            new SynchronizerSupport()
    135            {
    136                @Override
    137                public void unlock(final LockMode mode)
    138                {
    139                }
    140
    141                @Override
    142                public void setSynchronizer(final Synchronizer sync)
    143                {
    144                }
    145
    146                @Override
    147                public void lock(final LockMode mode)
    148                {
    149                }
    150
    151                @Override
    152                public Synchronizer getSynchronizer()
    153                {
    154                    return NoOpSynchronizer.INSTANCE;
    155                }
    156            };
    157
    158    /** The file-based object managed by this handler. */
    159    private final FileBased content;
    160
    161    /** A reference to the current {@code FileLocator} object. */
    162    private final AtomicReference<FileLocator> fileLocator;
    163
    164    /** A collection with the registered listeners. */
    165    private final List<FileHandlerListener> listeners =
    166            new CopyOnWriteArrayList<>();
    167
    168    /**
    169     * Creates a new instance of {@code FileHandler} which is not associated
    170     * with a {@code FileBased} object and thus does not have a content. Objects
    171     * of this kind can be used to define a file location, but it is not
    172     * possible to actually load or save data.
    173     */
    174    public FileHandler()
    175    {
    176        this(null);
    177    }
    178
    179    /**
    180     * Creates a new instance of {@code FileHandler} and sets the managed
    181     * {@code FileBased} object.
    182     *
    183     * @param obj the file-based object to manage
    184     */
    185    public FileHandler(final FileBased obj)
    186    {
    187        this(obj, emptyFileLocator());
    188    }
    189
    190    /**
    191     * Creates a new instance of {@code FileHandler} which is associated with
    192     * the given {@code FileBased} object and the location defined for the given
    193     * {@code FileHandler} object. A copy of the location of the given
    194     * {@code FileHandler} is created. This constructor is a possibility to
    195     * associate a file location with a {@code FileBased} object.
    196     *
    197     * @param obj the {@code FileBased} object to manage
    198     * @param c the {@code FileHandler} from which to copy the location (must
    199     *        not be <b>null</b>)
    200     * @throws IllegalArgumentException if the {@code FileHandler} is
    201     *         <b>null</b>
    202     */
    203    public FileHandler(final FileBased obj, final FileHandler c)
    204    {
    205        this(obj, checkSourceHandler(c).getFileLocator());
    206    }
    207
    208    /**
    209     * Creates a new instance of {@code FileHandler} based on the given
    210     * {@code FileBased} and {@code FileLocator} objects.
    211     *
    212     * @param obj the {@code FileBased} object to manage
    213     * @param locator the {@code FileLocator}
    214     */
    215    private FileHandler(final FileBased obj, final FileLocator locator)
    216    {
    217        content = obj;
    218        fileLocator = new AtomicReference<>(locator);
    219    }
    220
    221    /**
    222     * Creates a new {@code FileHandler} instance from properties stored in a
    223     * map. This method tries to extract a {@link FileLocator} from the map. A
    224     * new {@code FileHandler} is created based on this {@code FileLocator}.
    225     *
    226     * @param map the map (may be <b>null</b>)
    227     * @return the newly created {@code FileHandler}
    228     * @see FileLocatorUtils#fromMap(Map)
    229     */
    230    public static FileHandler fromMap(final Map<String, ?> map)
    231    {
    232        return new FileHandler(null, FileLocatorUtils.fromMap(map));
    233    }
    234
    235    /**
    236     * Returns the {@code FileBased} object associated with this
    237     * {@code FileHandler}.
    238     *
    239     * @return the associated {@code FileBased} object
    240     */
    241    public final FileBased getContent()
    242    {
    243        return content;
    244    }
    245
    246    /**
    247     * Adds a listener to this {@code FileHandler}. It is notified about
    248     * property changes and IO operations.
    249     *
    250     * @param l the listener to be added (must not be <b>null</b>)
    251     * @throws IllegalArgumentException if the listener is <b>null</b>
    252     */
    253    public void addFileHandlerListener(final FileHandlerListener l)
    254    {
    255        if (l == null)
    256        {
    257            throw new IllegalArgumentException("Listener must not be null!");
    258        }
    259        listeners.add(l);
    260    }
    261
    262    /**
    263     * Removes the specified listener from this object.
    264     *
    265     * @param l the listener to be removed
    266     */
    267    public void removeFileHandlerListener(final FileHandlerListener l)
    268    {
    269        listeners.remove(l);
    270    }
    271
    272    /**
    273     * Return the name of the file. If only a URL is defined, the file name
    274     * is derived from there.
    275     *
    276     * @return the file name
    277     */
    278    public String getFileName()
    279    {
    280        final FileLocator locator = getFileLocator();
    281        if (locator.getFileName() != null)
    282        {
    283            return locator.getFileName();
    284        }
    285
    286        if (locator.getSourceURL() != null)
    287        {
    288            return FileLocatorUtils.getFileName(locator.getSourceURL());
    289        }
    290
    291        return null;
    292    }
    293
    294    /**
    295     * Set the name of the file. The passed in file name can contain a relative
    296     * path. It must be used when referring files with relative paths from
    297     * classpath. Use {@code setPath()} to set a full qualified file name. The
    298     * URL is set to <b>null</b> as it has to be determined anew based on the
    299     * file name and the base path.
    300     *
    301     * @param fileName the name of the file
    302     */
    303    public void setFileName(final String fileName)
    304    {
    305        final String name = normalizeFileURL(fileName);
    306        new Updater()
    307        {
    308            @Override
    309            protected void updateBuilder(final FileLocatorBuilder builder)
    310            {
    311                builder.fileName(name);
    312                builder.sourceURL(null);
    313            }
    314        }
    315        .update();
    316    }
    317
    318    /**
    319     * Return the base path. If no base path is defined, but a URL, the base
    320     * path is derived from there.
    321     *
    322     * @return the base path
    323     */
    324    public String getBasePath()
    325    {
    326        final FileLocator locator = getFileLocator();
    327        if (locator.getBasePath() != null)
    328        {
    329            return locator.getBasePath();
    330        }
    331
    332        if (locator.getSourceURL() != null)
    333        {
    334            return FileLocatorUtils.getBasePath(locator.getSourceURL());
    335        }
    336
    337        return null;
    338    }
    339
    340    /**
    341     * Sets the base path. The base path is typically either a path to a
    342     * directory or a URL. Together with the value passed to the
    343     * {@code setFileName()} method it defines the location of the configuration
    344     * file to be loaded. The strategies for locating the file are quite
    345     * tolerant. For instance if the file name is already an absolute path or a
    346     * fully defined URL, the base path will be ignored. The base path can also
    347     * be a URL, in which case the file name is interpreted in this URL's
    348     * context. If other methods are used for determining the location of the
    349     * associated file (e.g. {@code setFile()} or {@code setURL()}), the base
    350     * path is automatically set. Setting the base path using this method
    351     * automatically sets the URL to <b>null</b> because it has to be
    352     * determined anew based on the file name and the base path.
    353     *
    354     * @param basePath the base path.
    355     */
    356    public void setBasePath(final String basePath)
    357    {
    358        final String path = normalizeFileURL(basePath);
    359        new Updater()
    360        {
    361            @Override
    362            protected void updateBuilder(final FileLocatorBuilder builder)
    363            {
    364                builder.basePath(path);
    365                builder.sourceURL(null);
    366            }
    367        }
    368        .update();
    369    }
    370
    371    /**
    372     * Returns the location of the associated file as a {@code File} object. If
    373     * the base path is a URL with a protocol different than &quot;file&quot;,
    374     * or the file is within a compressed archive, the return value will not
    375     * point to a valid file object.
    376     *
    377     * @return the location as {@code File} object; this can be <b>null</b>
    378     */
    379    public File getFile()
    380    {
    381        return createFile(getFileLocator());
    382    }
    383
    384    /**
    385     * Sets the location of the associated file as a {@code File} object. The
    386     * passed in {@code File} is made absolute if it is not yet. Then the file's
    387     * path component becomes the base path and its name component becomes the
    388     * file name.
    389     *
    390     * @param file the location of the associated file
    391     */
    392    public void setFile(final File file)
    393    {
    394        final String fileName = file.getName();
    395        final String basePath =
    396                (file.getParentFile() != null) ? file.getParentFile()
    397                        .getAbsolutePath() : null;
    398        new Updater()
    399        {
    400            @Override
    401            protected void updateBuilder(final FileLocatorBuilder builder)
    402            {
    403                builder.fileName(fileName).basePath(basePath).sourceURL(null);
    404            }
    405        }
    406        .update();
    407    }
    408
    409    /**
    410     * Returns the full path to the associated file. The return value is a valid
    411     * {@code File} path only if this location is based on a file on the local
    412     * disk. If the file was loaded from a packed archive, the returned value is
    413     * the string form of the URL from which the file was loaded.
    414     *
    415     * @return the full path to the associated file
    416     */
    417    public String getPath()
    418    {
    419        final FileLocator locator = getFileLocator();
    420        final File file = createFile(locator);
    421        return FileLocatorUtils.obtainFileSystem(locator).getPath(file,
    422                locator.getSourceURL(), locator.getBasePath(), locator.getFileName());
    423    }
    424
    425    /**
    426     * Sets the location of the associated file as a full or relative path name.
    427     * The passed in path should represent a valid file name on the file system.
    428     * It must not be used to specify relative paths for files that exist in
    429     * classpath, either plain file system or compressed archive, because this
    430     * method expands any relative path to an absolute one which may end in an
    431     * invalid absolute path for classpath references.
    432     *
    433     * @param path the full path name of the associated file
    434     */
    435    public void setPath(final String path)
    436    {
    437        setFile(new File(path));
    438    }
    439
    440    /**
    441     * Returns the location of the associated file as a URL. If a URL is set,
    442     * it is directly returned. Otherwise, an attempt to locate the referenced
    443     * file is made.
    444     *
    445     * @return a URL to the associated file; can be <b>null</b> if the location
    446     *         is unspecified
    447     */
    448    public URL getURL()
    449    {
    450        final FileLocator locator = getFileLocator();
    451        return (locator.getSourceURL() != null) ? locator.getSourceURL()
    452                : FileLocatorUtils.locate(locator);
    453    }
    454
    455    /**
    456     * Sets the location of the associated file as a URL. For loading this can
    457     * be an arbitrary URL with a supported protocol. If the file is to be
    458     * saved, too, a URL with the &quot;file&quot; protocol should be provided.
    459     * This method sets the file name and the base path to <b>null</b>.
    460     * They have to be determined anew based on the new URL.
    461     *
    462     * @param url the location of the file as URL
    463     */
    464    public void setURL(final URL url)
    465    {
    466        new Updater()
    467        {
    468            @Override
    469            protected void updateBuilder(final FileLocatorBuilder builder)
    470            {
    471                builder.sourceURL(url);
    472                builder.basePath(null).fileName(null);
    473            }
    474        }
    475        .update();
    476    }
    477
    478    /**
    479     * Returns a {@code FileLocator} object with the specification of the file
    480     * stored by this {@code FileHandler}. Note that this method returns the
    481     * internal data managed by this {@code FileHandler} as it was defined.
    482     * This is not necessarily the same as the data returned by the single
    483     * access methods like {@code getFileName()} or {@code getURL()}: These
    484     * methods try to derive missing data from other values that have been set.
    485     *
    486     * @return a {@code FileLocator} with the referenced file
    487     */
    488    public FileLocator getFileLocator()
    489    {
    490        return fileLocator.get();
    491    }
    492
    493    /**
    494     * Sets the file to be accessed by this {@code FileHandler} as a
    495     * {@code FileLocator} object.
    496     *
    497     * @param locator the {@code FileLocator} with the definition of the file to
    498     *        be accessed (must not be <b>null</b>
    499     * @throws IllegalArgumentException if the {@code FileLocator} is
    500     *         <b>null</b>
    501     */
    502    public void setFileLocator(final FileLocator locator)
    503    {
    504        if (locator == null)
    505        {
    506            throw new IllegalArgumentException("FileLocator must not be null!");
    507        }
    508
    509        fileLocator.set(locator);
    510        fireLocationChangedEvent();
    511    }
    512
    513    /**
    514     * Tests whether a location is defined for this {@code FileHandler}.
    515     *
    516     * @return <b>true</b> if a location is defined, <b>false</b> otherwise
    517     */
    518    public boolean isLocationDefined()
    519    {
    520        return FileLocatorUtils.isLocationDefined(getFileLocator());
    521    }
    522
    523    /**
    524     * Clears the location of this {@code FileHandler}. Afterwards this handler
    525     * does not point to any valid file.
    526     */
    527    public void clearLocation()
    528    {
    529        new Updater()
    530        {
    531            @Override
    532            protected void updateBuilder(final FileLocatorBuilder builder)
    533            {
    534                builder.basePath(null).fileName(null).sourceURL(null);
    535            }
    536        }
    537        .update();
    538    }
    539
    540    /**
    541     * Returns the encoding of the associated file. Result can be <b>null</b> if
    542     * no encoding has been set.
    543     *
    544     * @return the encoding of the associated file
    545     */
    546    public String getEncoding()
    547    {
    548        return getFileLocator().getEncoding();
    549    }
    550
    551    /**
    552     * Sets the encoding of the associated file. The encoding applies if binary
    553     * files are loaded. Note that in this case setting an encoding is
    554     * recommended; otherwise the platform's default encoding is used.
    555     *
    556     * @param encoding the encoding of the associated file
    557     */
    558    public void setEncoding(final String encoding)
    559    {
    560        new Updater()
    561        {
    562            @Override
    563            protected void updateBuilder(final FileLocatorBuilder builder)
    564            {
    565                builder.encoding(encoding);
    566            }
    567        }
    568        .update();
    569    }
    570
    571    /**
    572     * Returns the {@code FileSystem} to be used by this object when locating
    573     * files. Result is never <b>null</b>; if no file system has been set, the
    574     * default file system is returned.
    575     *
    576     * @return the used {@code FileSystem}
    577     */
    578    public FileSystem getFileSystem()
    579    {
    580        return FileLocatorUtils.obtainFileSystem(getFileLocator());
    581    }
    582
    583    /**
    584     * Sets the {@code FileSystem} to be used by this object when locating
    585     * files. If a <b>null</b> value is passed in, the file system is reset to
    586     * the default file system.
    587     *
    588     * @param fileSystem the {@code FileSystem}
    589     */
    590    public void setFileSystem(final FileSystem fileSystem)
    591    {
    592        new Updater()
    593        {
    594            @Override
    595            protected void updateBuilder(final FileLocatorBuilder builder)
    596            {
    597                builder.fileSystem(fileSystem);
    598            }
    599        }
    600        .update();
    601    }
    602
    603    /**
    604     * Resets the {@code FileSystem} used by this object. It is set to the
    605     * default file system.
    606     */
    607    public void resetFileSystem()
    608    {
    609        setFileSystem(null);
    610    }
    611
    612    /**
    613     * Returns the {@code FileLocationStrategy} to be applied when accessing the
    614     * associated file. This method never returns <b>null</b>. If a
    615     * {@code FileLocationStrategy} has been set, it is returned. Otherwise,
    616     * result is the default {@code FileLocationStrategy}.
    617     *
    618     * @return the {@code FileLocationStrategy} to be used
    619     */
    620    public FileLocationStrategy getLocationStrategy()
    621    {
    622        return FileLocatorUtils.obtainLocationStrategy(getFileLocator());
    623    }
    624
    625    /**
    626     * Sets the {@code FileLocationStrategy} to be applied when accessing the
    627     * associated file. The strategy is stored in the underlying
    628     * {@link FileLocator}. The argument can be <b>null</b>; this causes the
    629     * default {@code FileLocationStrategy} to be used.
    630     *
    631     * @param strategy the {@code FileLocationStrategy}
    632     * @see FileLocatorUtils#DEFAULT_LOCATION_STRATEGY
    633     */
    634    public void setLocationStrategy(final FileLocationStrategy strategy)
    635    {
    636        new Updater()
    637        {
    638            @Override
    639            protected void updateBuilder(final FileLocatorBuilder builder)
    640            {
    641                builder.locationStrategy(strategy);
    642            }
    643
    644        }
    645        .update();
    646    }
    647
    648    /**
    649     * Locates the referenced file if necessary and ensures that the associated
    650     * {@link FileLocator} is fully initialized. When accessing the referenced
    651     * file the information stored in the associated {@code FileLocator} is
    652     * used. If this information is incomplete (e.g. only the file name is set),
    653     * an attempt to locate the file may have to be performed on each access. By
    654     * calling this method such an attempt is performed once, and the results of
    655     * a successful localization are stored. Hence, later access to the
    656     * referenced file can be more efficient. Also, all properties pointing to
    657     * the referenced file in this object's {@code FileLocator} are set (i.e.
    658     * the URL, the base path, and the file name). If the referenced file cannot
    659     * be located, result is <b>false</b>. This means that the information in
    660     * the current {@code FileLocator} is insufficient or wrong. If the
    661     * {@code FileLocator} is already fully defined, it is not changed.
    662     *
    663     * @return a flag whether the referenced file could be located successfully
    664     * @see FileLocatorUtils#fullyInitializedLocator(FileLocator)
    665     */
    666    public boolean locate()
    667    {
    668        boolean result;
    669        boolean done;
    670
    671        do
    672        {
    673            final FileLocator locator = getFileLocator();
    674            FileLocator fullLocator =
    675                    FileLocatorUtils.fullyInitializedLocator(locator);
    676            if (fullLocator == null)
    677            {
    678                result = false;
    679                fullLocator = locator;
    680            }
    681            else
    682            {
    683                result =
    684                        fullLocator != locator
    685                                || FileLocatorUtils.isFullyInitialized(locator);
    686            }
    687            done = fileLocator.compareAndSet(locator, fullLocator);
    688        } while (!done);
    689
    690        return result;
    691    }
    692
    693    /**
    694     * Loads the associated file from the underlying location. If no location
    695     * has been set, an exception is thrown.
    696     *
    697     * @throws ConfigurationException if loading of the configuration fails
    698     */
    699    public void load() throws ConfigurationException
    700    {
    701        load(checkContentAndGetLocator());
    702    }
    703
    704    /**
    705     * Loads the associated file from the given file name. The file name is
    706     * interpreted in the context of the already set location (e.g. if it is a
    707     * relative file name, a base path is applied if available). The underlying
    708     * location is not changed.
    709     *
    710     * @param fileName the name of the file to be loaded
    711     * @throws ConfigurationException if an error occurs
    712     */
    713    public void load(final String fileName) throws ConfigurationException
    714    {
    715        load(fileName, checkContentAndGetLocator());
    716    }
    717
    718    /**
    719     * Loads the associated file from the specified {@code File}.
    720     *
    721     * @param file the file to load
    722     * @throws ConfigurationException if an error occurs
    723     */
    724    public void load(final File file) throws ConfigurationException
    725    {
    726        URL url;
    727        try
    728        {
    729            url = FileLocatorUtils.toURL(file);
    730        }
    731        catch (final MalformedURLException e1)
    732        {
    733            throw new ConfigurationException("Cannot create URL from file "
    734                    + file);
    735        }
    736
    737        load(url);
    738    }
    739
    740    /**
    741     * Loads the associated file from the specified URL. The location stored in
    742     * this object is not changed.
    743     *
    744     * @param url the URL of the file to be loaded
    745     * @throws ConfigurationException if an error occurs
    746     */
    747    public void load(final URL url) throws ConfigurationException
    748    {
    749        load(url, checkContentAndGetLocator());
    750    }
    751
    752    /**
    753     * Loads the associated file from the specified stream, using the encoding
    754     * returned by {@link #getEncoding()}.
    755     *
    756     * @param in the input stream
    757     * @throws ConfigurationException if an error occurs during the load
    758     *         operation
    759     */
    760    public void load(final InputStream in) throws ConfigurationException
    761    {
    762        load(in, checkContentAndGetLocator());
    763    }
    764
    765    /**
    766     * Loads the associated file from the specified stream, using the specified
    767     * encoding. If the encoding is <b>null</b>, the default encoding is used.
    768     *
    769     * @param in the input stream
    770     * @param encoding the encoding used, {@code null} to use the default
    771     *        encoding
    772     * @throws ConfigurationException if an error occurs during the load
    773     *         operation
    774     */
    775    public void load(final InputStream in, final String encoding)
    776            throws ConfigurationException
    777    {
    778        loadFromStream(in, encoding, null);
    779    }
    780
    781    /**
    782     * Loads the associated file from the specified reader.
    783     *
    784     * @param in the reader
    785     * @throws ConfigurationException if an error occurs during the load
    786     *         operation
    787     */
    788    public void load(final Reader in) throws ConfigurationException
    789    {
    790        checkContent();
    791        injectNullFileLocator();
    792        loadFromReader(in);
    793    }
    794
    795    /**
    796     * Saves the associated file to the current location set for this object.
    797     * Before this method can be called a valid location must have been set.
    798     *
    799     * @throws ConfigurationException if an error occurs or no location has been
    800     *         set yet
    801     */
    802    public void save() throws ConfigurationException
    803    {
    804        save(checkContentAndGetLocator());
    805    }
    806
    807    /**
    808     * Saves the associated file to the specified file name. This does not
    809     * change the location of this object (use {@link #setFileName(String)} if
    810     * you need it).
    811     *
    812     * @param fileName the file name
    813     * @throws ConfigurationException if an error occurs during the save
    814     *         operation
    815     */
    816    public void save(final String fileName) throws ConfigurationException
    817    {
    818        save(fileName, checkContentAndGetLocator());
    819    }
    820
    821    /**
    822     * Saves the associated file to the specified URL. This does not change the
    823     * location of this object (use {@link #setURL(URL)} if you need it).
    824     *
    825     * @param url the URL
    826     * @throws ConfigurationException if an error occurs during the save
    827     *         operation
    828     */
    829    public void save(final URL url) throws ConfigurationException
    830    {
    831        save(url, checkContentAndGetLocator());
    832    }
    833
    834    /**
    835     * Saves the associated file to the specified {@code File}. The file is
    836     * created automatically if it doesn't exist. This does not change the
    837     * location of this object (use {@link #setFile} if you need it).
    838     *
    839     * @param file the target file
    840     * @throws ConfigurationException if an error occurs during the save
    841     *         operation
    842     */
    843    public void save(final File file) throws ConfigurationException
    844    {
    845        save(file, checkContentAndGetLocator());
    846    }
    847
    848    /**
    849     * Saves the associated file to the specified stream using the encoding
    850     * returned by {@link #getEncoding()}.
    851     *
    852     * @param out the output stream
    853     * @throws ConfigurationException if an error occurs during the save
    854     *         operation
    855     */
    856    public void save(final OutputStream out) throws ConfigurationException
    857    {
    858        save(out, checkContentAndGetLocator());
    859    }
    860
    861    /**
    862     * Saves the associated file to the specified stream using the specified
    863     * encoding. If the encoding is <b>null</b>, the default encoding is used.
    864     *
    865     * @param out the output stream
    866     * @param encoding the encoding to be used, {@code null} to use the default
    867     *        encoding
    868     * @throws ConfigurationException if an error occurs during the save
    869     *         operation
    870     */
    871    public void save(final OutputStream out, final String encoding)
    872            throws ConfigurationException
    873    {
    874        saveToStream(out, encoding, null);
    875    }
    876
    877    /**
    878     * Saves the associated file to the given {@code Writer}.
    879     *
    880     * @param out the {@code Writer}
    881     * @throws ConfigurationException if an error occurs during the save
    882     *         operation
    883     */
    884    public void save(final Writer out) throws ConfigurationException
    885    {
    886        checkContent();
    887        injectNullFileLocator();
    888        saveToWriter(out);
    889    }
    890
    891    /**
    892     * Prepares a builder for a {@code FileLocator} which does not have a
    893     * defined file location. Other properties (e.g. encoding or file system)
    894     * are initialized from the {@code FileLocator} associated with this object.
    895     *
    896     * @return the initialized builder for a {@code FileLocator}
    897     */
    898    private FileLocatorBuilder prepareNullLocatorBuilder()
    899    {
    900        return FileLocatorUtils.fileLocator(getFileLocator()).sourceURL(null)
    901                .basePath(null).fileName(null);
    902    }
    903
    904    /**
    905     * Checks whether the associated {@code FileBased} object implements the
    906     * {@code FileLocatorAware} interface. If this is the case, a
    907     * {@code FileLocator} instance is injected which returns only <b>null</b>
    908     * values. This method is called if no file location is available (e.g. if
    909     * data is to be loaded from a stream). The encoding of the injected locator
    910     * is derived from this object.
    911     */
    912    private void injectNullFileLocator()
    913    {
    914        if (getContent() instanceof FileLocatorAware)
    915        {
    916            final FileLocator locator = prepareNullLocatorBuilder().create();
    917            ((FileLocatorAware) getContent()).initFileLocator(locator);
    918        }
    919    }
    920
    921    /**
    922     * Injects a {@code FileLocator} pointing to the specified URL if the
    923     * current {@code FileBased} object implements the {@code FileLocatorAware}
    924     * interface.
    925     *
    926     * @param url the URL for the locator
    927     */
    928    private void injectFileLocator(final URL url)
    929    {
    930        if (url == null)
    931        {
    932            injectNullFileLocator();
    933        }
    934        else
    935        {
    936            if (getContent() instanceof FileLocatorAware)
    937            {
    938                final FileLocator locator =
    939                        prepareNullLocatorBuilder().sourceURL(url).create();
    940                ((FileLocatorAware) getContent()).initFileLocator(locator);
    941            }
    942        }
    943    }
    944
    945    /**
    946     * Obtains a {@code SynchronizerSupport} for the current content. If the
    947     * content implements this interface, it is returned. Otherwise, result is a
    948     * dummy object. This method is called before load and save operations. The
    949     * returned object is used for synchronization.
    950     *
    951     * @return the {@code SynchronizerSupport} for synchronization
    952     */
    953    private SynchronizerSupport fetchSynchronizerSupport()
    954    {
    955        if (getContent() instanceof SynchronizerSupport)
    956        {
    957            return (SynchronizerSupport) getContent();
    958        }
    959        return DUMMY_SYNC_SUPPORT;
    960    }
    961
    962    /**
    963     * Internal helper method for loading the associated file from the location
    964     * specified in the given {@code FileLocator}.
    965     *
    966     * @param locator the current {@code FileLocator}
    967     * @throws ConfigurationException if an error occurs
    968     */
    969    private void load(final FileLocator locator) throws ConfigurationException
    970    {
    971        final URL url = FileLocatorUtils.locateOrThrow(locator);
    972        load(url, locator);
    973    }
    974
    975    /**
    976     * Internal helper method for loading a file from the given URL.
    977     *
    978     * @param url the URL
    979     * @param locator the current {@code FileLocator}
    980     * @throws ConfigurationException if an error occurs
    981     */
    982    private void load(final URL url, final FileLocator locator) throws ConfigurationException
    983    {
    984        InputStream in = null;
    985
    986        try
    987        {
    988            in = FileLocatorUtils.obtainFileSystem(locator).getInputStream(url);
    989            loadFromStream(in, locator.getEncoding(), url);
    990        }
    991        catch (final ConfigurationException e)
    992        {
    993            throw e;
    994        }
    995        catch (final Exception e)
    996        {
    997            throw new ConfigurationException(
    998                    "Unable to load the configuration from the URL " + url, e);
    999        }
    1000        finally
    1001        {
    1002            closeSilent(in);
    1003        }
    1004    }
    1005
    1006    /**
    1007     * Internal helper method for loading a file from a file name.
    1008     *
    1009     * @param fileName the file name
    1010     * @param locator the current {@code FileLocator}
    1011     * @throws ConfigurationException if an error occurs
    1012     */
    1013    private void load(final String fileName, final FileLocator locator)
    1014            throws ConfigurationException
    1015    {
    1016        final FileLocator locFileName = createLocatorWithFileName(fileName, locator);
    1017        final URL url = FileLocatorUtils.locateOrThrow(locFileName);
    1018        load(url, locator);
    1019    }
    1020
    1021    /**
    1022     * Internal helper method for loading a file from the given input stream.
    1023     *
    1024     * @param in the input stream
    1025     * @param locator the current {@code FileLocator}
    1026     * @throws ConfigurationException if an error occurs
    1027     */
    1028    private void load(final InputStream in, final FileLocator locator)
    1029            throws ConfigurationException
    1030    {
    1031        load(in, locator.getEncoding());
    1032    }
    1033
    1034    /**
    1035     * Internal helper method for loading a file from an input stream.
    1036     *
    1037     * @param in the input stream
    1038     * @param encoding the encoding
    1039     * @param url the URL of the file to be loaded (if known)
    1040     * @throws ConfigurationException if an error occurs
    1041     */
    1042    private void loadFromStream(final InputStream in, final String encoding, final URL url)
    1043            throws ConfigurationException
    1044    {
    1045        checkContent();
    1046        final SynchronizerSupport syncSupport = fetchSynchronizerSupport();
    1047        syncSupport.lock(LockMode.WRITE);
    1048        try
    1049        {
    1050            injectFileLocator(url);
    1051
    1052            if (getContent() instanceof InputStreamSupport)
    1053            {
    1054                loadFromStreamDirectly(in);
    1055            }
    1056            else
    1057            {
    1058                loadFromTransformedStream(in, encoding);
    1059            }
    1060        }
    1061        finally
    1062        {
    1063            syncSupport.unlock(LockMode.WRITE);
    1064        }
    1065    }
    1066
    1067    /**
    1068     * Loads data from an input stream if the associated {@code FileBased}
    1069     * object implements the {@code InputStreamSupport} interface.
    1070     *
    1071     * @param in the input stream
    1072     * @throws ConfigurationException if an error occurs
    1073     */
    1074    private void loadFromStreamDirectly(final InputStream in)
    1075            throws ConfigurationException
    1076    {
    1077        try
    1078        {
    1079            ((InputStreamSupport) getContent()).read(in);
    1080        }
    1081        catch (final IOException e)
    1082        {
    1083            throw new ConfigurationException(e);
    1084        }
    1085    }
    1086
    1087    /**
    1088     * Internal helper method for transforming an input stream to a reader and
    1089     * reading its content.
    1090     *
    1091     * @param in the input stream
    1092     * @param encoding the encoding
    1093     * @throws ConfigurationException if an error occurs
    1094     */
    1095    private void loadFromTransformedStream(final InputStream in, final String encoding)
    1096            throws ConfigurationException
    1097    {
    1098        Reader reader = null;
    1099
    1100        if (encoding != null)
    1101        {
    1102            try
    1103            {
    1104                reader = new InputStreamReader(in, encoding);
    1105            }
    1106            catch (final UnsupportedEncodingException e)
    1107            {
    1108                throw new ConfigurationException(
    1109                        "The requested encoding is not supported, try the default encoding.",
    1110                        e);
    1111            }
    1112        }
    1113
    1114        if (reader == null)
    1115        {
    1116            reader = new InputStreamReader(in);
    1117        }
    1118
    1119        loadFromReader(reader);
    1120    }
    1121
    1122    /**
    1123     * Internal helper method for loading a file from the given reader.
    1124     *
    1125     * @param in the reader
    1126     * @throws ConfigurationException if an error occurs
    1127     */
    1128    private void loadFromReader(final Reader in) throws ConfigurationException
    1129    {
    1130        fireLoadingEvent();
    1131        try
    1132        {
    1133            getContent().read(in);
    1134        }
    1135        catch (final IOException ioex)
    1136        {
    1137            throw new ConfigurationException(ioex);
    1138        }
    1139        finally
    1140        {
    1141            fireLoadedEvent();
    1142        }
    1143    }
    1144
    1145    /**
    1146     * Internal helper method for saving data to the internal location stored
    1147     * for this object.
    1148     *
    1149     * @param locator the current {@code FileLocator}
    1150     * @throws ConfigurationException if an error occurs during the save
    1151     *         operation
    1152     */
    1153    private void save(final FileLocator locator) throws ConfigurationException
    1154    {
    1155        if (!FileLocatorUtils.isLocationDefined(locator))
    1156        {
    1157            throw new ConfigurationException("No file location has been set!");
    1158        }
    1159
    1160        if (locator.getSourceURL() != null)
    1161        {
    1162            save(locator.getSourceURL(), locator);
    1163        }
    1164        else
    1165        {
    1166            save(locator.getFileName(), locator);
    1167        }
    1168    }
    1169
    1170    /**
    1171     * Internal helper method for saving data to the given file name.
    1172     *
    1173     * @param fileName the path to the target file
    1174     * @param locator the current {@code FileLocator}
    1175     * @throws ConfigurationException if an error occurs during the save
    1176     *         operation
    1177     */
    1178    private void save(final String fileName, final FileLocator locator)
    1179            throws ConfigurationException
    1180    {
    1181        URL url;
    1182        try
    1183        {
    1184            url = FileLocatorUtils.obtainFileSystem(locator).getURL(
    1185                    locator.getBasePath(), fileName);
    1186        }
    1187        catch (final MalformedURLException e)
    1188        {
    1189            throw new ConfigurationException(e);
    1190        }
    1191
    1192        if (url == null)
    1193        {
    1194            throw new ConfigurationException(
    1195                    "Cannot locate configuration source " + fileName);
    1196        }
    1197        save(url, locator);
    1198    }
    1199
    1200    /**
    1201     * Internal helper method for saving data to the given URL.
    1202     *
    1203     * @param url the target URL
    1204     * @param locator the {@code FileLocator}
    1205     * @throws ConfigurationException if an error occurs during the save
    1206     *         operation
    1207     */
    1208    private void save(final URL url, final FileLocator locator) throws ConfigurationException
    1209    {
    1210        OutputStream out = null;
    1211        try
    1212        {
    1213            out = FileLocatorUtils.obtainFileSystem(locator).getOutputStream(url);
    1214            saveToStream(out, locator.getEncoding(), url);
    1215            if (out instanceof VerifiableOutputStream)
    1216            {
    1217                try
    1218                {
    1219                    ((VerifiableOutputStream) out).verify();
    1220                }
    1221                catch (final IOException e)
    1222                {
    1223                    throw new ConfigurationException(e);
    1224                }
    1225            }
    1226        }
    1227        finally
    1228        {
    1229            closeSilent(out);
    1230        }
    1231    }
    1232
    1233    /**
    1234     * Internal helper method for saving data to the given {@code File}.
    1235     *
    1236     * @param file the target file
    1237     * @param locator the current {@code FileLocator}
    1238     * @throws ConfigurationException if an error occurs during the save
    1239     *         operation
    1240     */
    1241    private void save(final File file, final FileLocator locator) throws ConfigurationException
    1242    {
    1243        OutputStream out = null;
    1244
    1245        try
    1246        {
    1247            out = FileLocatorUtils.obtainFileSystem(locator).getOutputStream(file);
    1248            saveToStream(out, locator.getEncoding(), file.toURI().toURL());
    1249        }
    1250        catch (final MalformedURLException muex)
    1251        {
    1252            throw new ConfigurationException(muex);
    1253        }
    1254        finally
    1255        {
    1256            closeSilent(out);
    1257        }
    1258    }
    1259
    1260    /**
    1261     * Internal helper method for saving a file to the given output stream.
    1262     *
    1263     * @param out the output stream
    1264     * @param locator the current {@code FileLocator}
    1265     * @throws ConfigurationException if an error occurs during the save
    1266     *         operation
    1267     */
    1268    private void save(final OutputStream out, final FileLocator locator)
    1269            throws ConfigurationException
    1270    {
    1271        save(out, locator.getEncoding());
    1272    }
    1273
    1274    /**
    1275     * Internal helper method for saving a file to the given stream.
    1276     *
    1277     * @param out the output stream
    1278     * @param encoding the encoding
    1279     * @param url the URL of the output file if known
    1280     * @throws ConfigurationException if an error occurs
    1281     */
    1282    private void saveToStream(final OutputStream out, final String encoding, final URL url)
    1283            throws ConfigurationException
    1284    {
    1285        checkContent();
    1286        final SynchronizerSupport syncSupport = fetchSynchronizerSupport();
    1287        syncSupport.lock(LockMode.WRITE);
    1288        try
    1289        {
    1290            injectFileLocator(url);
    1291            Writer writer = null;
    1292
    1293            if (encoding != null)
    1294            {
    1295                try
    1296                {
    1297                    writer = new OutputStreamWriter(out, encoding);
    1298                }
    1299                catch (final UnsupportedEncodingException e)
    1300                {
    1301                    throw new ConfigurationException(
    1302                            "The requested encoding is not supported, try the default encoding.",
    1303                            e);
    1304                }
    1305            }
    1306
    1307            if (writer == null)
    1308            {
    1309                writer = new OutputStreamWriter(out);
    1310            }
    1311
    1312            saveToWriter(writer);
    1313        }
    1314        finally
    1315        {
    1316            syncSupport.unlock(LockMode.WRITE);
    1317        }
    1318    }
    1319
    1320    /**
    1321     * Internal helper method for saving a file into the given writer.
    1322     *
    1323     * @param out the writer
    1324     * @throws ConfigurationException if an error occurs
    1325     */
    1326    private void saveToWriter(final Writer out) throws ConfigurationException
    1327    {
    1328        fireSavingEvent();
    1329        try
    1330        {
    1331            getContent().write(out);
    1332        }
    1333        catch (final IOException ioex)
    1334        {
    1335            throw new ConfigurationException(ioex);
    1336        }
    1337        finally
    1338        {
    1339            fireSavedEvent();
    1340        }
    1341    }
    1342
    1343    /**
    1344     * Creates a {@code FileLocator} which is a copy of the passed in one, but
    1345     * has the given file name set to reference the target file.
    1346     *
    1347     * @param fileName the file name
    1348     * @param locator the {@code FileLocator} to copy
    1349     * @return the manipulated {@code FileLocator} with the file name
    1350     */
    1351    private FileLocator createLocatorWithFileName(final String fileName,
    1352            final FileLocator locator)
    1353    {
    1354        return FileLocatorUtils.fileLocator(locator).sourceURL(null)
    1355                .fileName(fileName).create();
    1356    }
    1357
    1358    /**
    1359     * Checks whether a content object is available. If not, an exception is
    1360     * thrown. This method is called whenever the content object is accessed.
    1361     *
    1362     * @throws ConfigurationException if not content object is defined
    1363     */
    1364    private void checkContent() throws ConfigurationException
    1365    {
    1366        if (getContent() == null)
    1367        {
    1368            throw new ConfigurationException("No content available!");
    1369        }
    1370    }
    1371
    1372    /**
    1373     * Checks whether a content object is available and returns the current
    1374     * {@code FileLocator}. If there is no content object, an exception is
    1375     * thrown. This is a typical operation to be performed before a load() or
    1376     * save() operation.
    1377     *
    1378     * @return the current {@code FileLocator} to be used for the calling
    1379     *         operation
    1380     */
    1381    private FileLocator checkContentAndGetLocator()
    1382            throws ConfigurationException
    1383    {
    1384        checkContent();
    1385        return getFileLocator();
    1386    }
    1387
    1388    /**
    1389     * Notifies the registered listeners about the start of a load operation.
    1390     */
    1391    private void fireLoadingEvent()
    1392    {
    1393        for (final FileHandlerListener l : listeners)
    1394        {
    1395            l.loading(this);
    1396        }
    1397    }
    1398
    1399    /**
    1400     * Notifies the registered listeners about a completed load operation.
    1401     */
    1402    private void fireLoadedEvent()
    1403    {
    1404        for (final FileHandlerListener l : listeners)
    1405        {
    1406            l.loaded(this);
    1407        }
    1408    }
    1409
    1410    /**
    1411     * Notifies the registered listeners about the start of a save operation.
    1412     */
    1413    private void fireSavingEvent()
    1414    {
    1415        for (final FileHandlerListener l : listeners)
    1416        {
    1417            l.saving(this);
    1418        }
    1419    }
    1420
    1421    /**
    1422     * Notifies the registered listeners about a completed save operation.
    1423     */
    1424    private void fireSavedEvent()
    1425    {
    1426        for (final FileHandlerListener l : listeners)
    1427        {
    1428            l.saved(this);
    1429        }
    1430    }
    1431
    1432    /**
    1433     * Notifies the registered listeners about a property update.
    1434     */
    1435    private void fireLocationChangedEvent()
    1436    {
    1437        for (final FileHandlerListener l : listeners)
    1438        {
    1439            l.locationChanged(this);
    1440        }
    1441    }
    1442
    1443    /**
    1444     * Normalizes URLs to files. Ensures that file URLs start with the correct
    1445     * protocol.
    1446     *
    1447     * @param fileName the string to be normalized
    1448     * @return the normalized file URL
    1449     */
    1450    private static String normalizeFileURL(String fileName)
    1451    {
    1452        if (fileName != null && fileName.startsWith(FILE_SCHEME)
    1453                && !fileName.startsWith(FILE_SCHEME_SLASH))
    1454        {
    1455            fileName =
    1456                    FILE_SCHEME_SLASH
    1457                            + fileName.substring(FILE_SCHEME.length());
    1458        }
    1459        return fileName;
    1460    }
    1461
    1462    /**
    1463     * A helper method for closing a stream. Occurring exceptions will be
    1464     * ignored.
    1465     *
    1466     * @param cl the stream to be closed (may be <b>null</b>)
    1467     */
    1468    private static void closeSilent(final Closeable cl)
    1469    {
    1470        try
    1471        {
    1472            if (cl != null)
    1473            {
    1474                cl.close();
    1475            }
    1476        }
    1477        catch (final IOException e)
    1478        {
    1479            LogFactory.getLog(FileHandler.class).warn("Exception when closing " + cl, e);
    1480        }
    1481    }
    1482
    1483    /**
    1484     * Creates a {@code File} object from the content of the given
    1485     * {@code FileLocator} object. If the locator is not defined, result is
    1486     * <b>null</b>.
    1487     *
    1488     * @param loc the {@code FileLocator}
    1489     * @return a {@code File} object pointing to the associated file
    1490     */
    1491    private static File createFile(final FileLocator loc)
    1492    {
    1493        if (loc.getFileName() == null && loc.getSourceURL() == null)
    1494        {
    1495            return null;
    1496        }
    1497        else if (loc.getSourceURL() != null)
    1498        {
    1499            return FileLocatorUtils.fileFromURL(loc.getSourceURL());
    1500        }
    1501        else
    1502        {
    1503            return FileLocatorUtils.getFile(loc.getBasePath(),
    1504                    loc.getFileName());
    1505        }
    1506    }
    1507
    1508    /**
    1509     * Creates an uninitialized file locator.
    1510     *
    1511     * @return the locator
    1512     */
    1513    private static FileLocator emptyFileLocator()
    1514    {
    1515        return FileLocatorUtils.fileLocator().create();
    1516    }
    1517
    1518    /**
    1519     * Helper method for checking a file handler which is to be copied. Throws
    1520     * an exception if the handler is <b>null</b>.
    1521     *
    1522     * @param c the {@code FileHandler} from which to copy the location
    1523     * @return the same {@code FileHandler}
    1524     */
    1525    private static FileHandler checkSourceHandler(final FileHandler c)
    1526    {
    1527        if (c == null)
    1528        {
    1529            throw new IllegalArgumentException(
    1530                    "FileHandler to assign must not be null!");
    1531        }
    1532        return c;
    1533    }
    1534
    1535    /**
    1536     * An internal class that performs all update operations of the handler's
    1537     * {@code FileLocator} in a safe way even if there is concurrent access.
    1538     * This class implements anon-blocking algorithm for replacing the immutable
    1539     * {@code FileLocator} instance stored in an atomic reference by a
    1540     * manipulated instance. (If we already had lambdas, this could be done
    1541     * without a class in a more elegant way.)
    1542     */
    1543    private abstract class Updater
    1544    {
    1545        /**
    1546         * Performs an update of the enclosing file handler's
    1547         * {@code FileLocator} object.
    1548         */
    1549        public void update()
    1550        {
    1551            boolean done;
    1552            do
    1553            {
    1554                final FileLocator oldLocator = fileLocator.get();
    1555                final FileLocatorBuilder builder =
    1556                        FileLocatorUtils.fileLocator(oldLocator);
    1557                updateBuilder(builder);
    1558                done = fileLocator.compareAndSet(oldLocator, builder.create());
    1559            } while (!done);
    1560            fireLocationChangedEvent();
    1561        }
    1562
    1563        /**
    1564         * Updates the passed in builder object to apply the manipulation to be
    1565         * performed by this {@code Updater}. The builder has been setup with
    1566         * the former content of the {@code FileLocator} to be manipulated.
    1567         *
    1568         * @param builder the builder for creating an updated
    1569         *        {@code FileLocator}
    1570         */
    1571        protected abstract void updateBuilder(FileLocatorBuilder builder);
    1572    }
    1573}