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