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