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