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