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