FileHandler.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.configuration2.io;

  18. import java.io.Closeable;
  19. import java.io.File;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.InputStreamReader;
  23. import java.io.OutputStream;
  24. import java.io.OutputStreamWriter;
  25. import java.io.Reader;
  26. import java.io.UnsupportedEncodingException;
  27. import java.io.Writer;
  28. import java.net.MalformedURLException;
  29. import java.net.URL;
  30. import java.util.List;
  31. import java.util.Map;
  32. import java.util.concurrent.CopyOnWriteArrayList;
  33. import java.util.concurrent.atomic.AtomicReference;

  34. import org.apache.commons.configuration2.ex.ConfigurationException;
  35. import org.apache.commons.configuration2.io.FileLocator.FileLocatorBuilder;
  36. import org.apache.commons.configuration2.sync.LockMode;
  37. import org.apache.commons.configuration2.sync.NoOpSynchronizer;
  38. import org.apache.commons.configuration2.sync.Synchronizer;
  39. import org.apache.commons.configuration2.sync.SynchronizerSupport;
  40. import org.apache.commons.logging.LogFactory;

  41. /**
  42.  * <p>
  43.  * A class that manages persistence of an associated {@link FileBased} object.
  44.  * </p>
  45.  * <p>
  46.  * Instances of this class can be used to load and save arbitrary objects implementing the {@code FileBased} interface
  47.  * in a convenient way from and to various locations. At construction time the {@code FileBased} object to manage is
  48.  * passed in. Basically, this object is assigned a location from which it is loaded and to which it can be saved. The
  49.  * following possibilities exist to specify such a location:
  50.  * </p>
  51.  * <ul>
  52.  * <li>URLs: With the method {@code setURL()} a full URL to the configuration source can be specified. This is the most
  53.  * flexible way. Note that the {@code save()} methods support only <em>file:</em> URLs.</li>
  54.  * <li>Files: The {@code setFile()} method allows to specify the configuration source as a file. This can be either a
  55.  * relative or an absolute file. In the former case the file is resolved based on the current directory.</li>
  56.  * <li>As file paths in string form: With the {@code setPath()} method a full path to a configuration file can be
  57.  * provided as a string.</li>
  58.  * <li>Separated as base path and file name: The base path is a string defining either a local directory or a URL. It
  59.  * can be set using the {@code setBasePath()} method. The file name, non surprisingly, defines the name of the
  60.  * configuration file.</li>
  61.  * </ul>
  62.  * <p>
  63.  * An instance stores a location. The {@code load()} and {@code save()} methods that do not take an argument make use of
  64.  * this internal location. Alternatively, it is also possible to use overloaded variants of {@code load()} and
  65.  * {@code save()} which expect a location. In these cases the location specified takes precedence over the internal one;
  66.  * the internal location is not changed.
  67.  * </p>
  68.  * <p>
  69.  * The actual position of the file to be loaded is determined by a {@link FileLocationStrategy} based on the location
  70.  * information that has been provided. By providing a custom location strategy the algorithm for searching files can be
  71.  * adapted. Save operations require more explicit information. They cannot rely on a location strategy because the file
  72.  * to be written may not yet exist. So there may be some differences in the way location information is interpreted by
  73.  * load and save operations. In order to avoid this, the following approach is recommended:
  74.  * </p>
  75.  * <ul>
  76.  * <li>Use the desired {@code setXXX()} methods to define the location of the file to be loaded.</li>
  77.  * <li>Call the {@code locate()} method. This method resolves the referenced file (if possible) and fills out all
  78.  * supported location information.</li>
  79.  * <li>Later on, {@code save()} can be called. This method now has sufficient information to store the file at the
  80.  * correct location.</li>
  81.  * </ul>
  82.  * <p>
  83.  * When loading or saving a {@code FileBased} object some additional functionality is performed if the object implements
  84.  * one of the following interfaces:
  85.  * </p>
  86.  * <ul>
  87.  * <li>{@code FileLocatorAware}: In this case an object with the current file location is injected before the load or
  88.  * save operation is executed. This is useful for {@code FileBased} objects that depend on their current location, for example
  89.  * to resolve relative path names.</li>
  90.  * <li>{@code SynchronizerSupport}: If this interface is implemented, load and save operations obtain a write lock on
  91.  * the {@code FileBased} object before they access it. (In case of a save operation, a read lock would probably be
  92.  * sufficient, but because of the possible injection of a {@link FileLocator} object it is not allowed to perform
  93.  * multiple save operations in parallel; therefore, by obtaining a write lock, we are on the safe side.)</li>
  94.  * </ul>
  95.  * <p>
  96.  * This class is thread-safe.
  97.  * </p>
  98.  *
  99.  * @since 2.0
  100.  */
  101. public class FileHandler {
  102.     /**
  103.      * An internal class that performs all update operations of the handler's {@code FileLocator} in a safe way even if
  104.      * there is concurrent access. This class implements anon-blocking algorithm for replacing the immutable
  105.      * {@code FileLocator} instance stored in an atomic reference by a manipulated instance. (If we already had lambdas,
  106.      * this could be done without a class in a more elegant way.)
  107.      */
  108.     private abstract class AbstractUpdater {
  109.         /**
  110.          * Performs an update of the enclosing file handler's {@code FileLocator} object.
  111.          */
  112.         public void update() {
  113.             boolean done;
  114.             do {
  115.                 final FileLocator oldLocator = fileLocator.get();
  116.                 final FileLocatorBuilder builder = FileLocatorUtils.fileLocator(oldLocator);
  117.                 updateBuilder(builder);
  118.                 done = fileLocator.compareAndSet(oldLocator, builder.create());
  119.             } while (!done);
  120.             fireLocationChangedEvent();
  121.         }

  122.         /**
  123.          * Updates the passed in builder object to apply the manipulation to be performed by this {@code Updater}. The builder
  124.          * has been setup with the former content of the {@code FileLocator} to be manipulated.
  125.          *
  126.          * @param builder the builder for creating an updated {@code FileLocator}
  127.          */
  128.         protected abstract void updateBuilder(FileLocatorBuilder builder);
  129.     }

  130.     /** Constant for the URI scheme for files. */
  131.     private static final String FILE_SCHEME = "file:";

  132.     /** Constant for the URI scheme for files with slashes. */
  133.     private static final String FILE_SCHEME_SLASH = FILE_SCHEME + "//";

  134.     /**
  135.      * A dummy implementation of {@code SynchronizerSupport}. This object is used when the file handler's content does not
  136.      * implement the {@code SynchronizerSupport} interface. All methods are just empty dummy implementations.
  137.      */
  138.     private static final SynchronizerSupport DUMMY_SYNC_SUPPORT = new SynchronizerSupport() {
  139.         @Override
  140.         public Synchronizer getSynchronizer() {
  141.             return NoOpSynchronizer.INSTANCE;
  142.         }

  143.         @Override
  144.         public void lock(final LockMode mode) {
  145.             // empty
  146.         }

  147.         @Override
  148.         public void setSynchronizer(final Synchronizer sync) {
  149.             // empty
  150.         }

  151.         @Override
  152.         public void unlock(final LockMode mode) {
  153.             // empty
  154.         }
  155.     };

  156.     /**
  157.      * Helper method for checking a file handler which is to be copied. Throws an exception if the handler is <strong>null</strong>.
  158.      *
  159.      * @param c the {@code FileHandler} from which to copy the location
  160.      * @return the same {@code FileHandler}
  161.      */
  162.     private static FileHandler checkSourceHandler(final FileHandler c) {
  163.         if (c == null) {
  164.             throw new IllegalArgumentException("FileHandler to assign must not be null!");
  165.         }
  166.         return c;
  167.     }

  168.     /**
  169.      * A helper method for closing a stream. Occurring exceptions will be ignored.
  170.      *
  171.      * @param cl the stream to be closed (may be <strong>null</strong>)
  172.      */
  173.     private static void closeSilent(final Closeable cl) {
  174.         try {
  175.             if (cl != null) {
  176.                 cl.close();
  177.             }
  178.         } catch (final IOException e) {
  179.             LogFactory.getLog(FileHandler.class).warn("Exception when closing " + cl, e);
  180.         }
  181.     }

  182.     /**
  183.      * Creates a {@code File} object from the content of the given {@code FileLocator} object. If the locator is not
  184.      * defined, result is <strong>null</strong>.
  185.      *
  186.      * @param loc the {@code FileLocator}
  187.      * @return a {@code File} object pointing to the associated file
  188.      */
  189.     private static File createFile(final FileLocator loc) {
  190.         if (loc.getFileName() == null && loc.getSourceURL() == null) {
  191.             return null;
  192.         }
  193.         if (loc.getSourceURL() != null) {
  194.             return FileLocatorUtils.fileFromURL(loc.getSourceURL());
  195.         }
  196.         return FileLocatorUtils.getFile(loc.getBasePath(), loc.getFileName());
  197.     }

  198.     /**
  199.      * Creates an uninitialized file locator.
  200.      *
  201.      * @return the locator
  202.      */
  203.     private static FileLocator emptyFileLocator() {
  204.         return FileLocatorUtils.fileLocator().create();
  205.     }

  206.     /**
  207.      * Creates a new {@code FileHandler} instance from properties stored in a map. This method tries to extract a
  208.      * {@link FileLocator} from the map. A new {@code FileHandler} is created based on this {@code FileLocator}.
  209.      *
  210.      * @param map the map (may be <strong>null</strong>)
  211.      * @return the newly created {@code FileHandler}
  212.      * @see FileLocatorUtils#fromMap(Map)
  213.      */
  214.     public static FileHandler fromMap(final Map<String, ?> map) {
  215.         return new FileHandler(null, FileLocatorUtils.fromMap(map));
  216.     }

  217.     /**
  218.      * Normalizes URLs to files. Ensures that file URLs start with the correct protocol.
  219.      *
  220.      * @param fileName the string to be normalized
  221.      * @return the normalized file URL
  222.      */
  223.     private static String normalizeFileURL(String fileName) {
  224.         if (fileName != null && fileName.startsWith(FILE_SCHEME) && !fileName.startsWith(FILE_SCHEME_SLASH)) {
  225.             fileName = FILE_SCHEME_SLASH + fileName.substring(FILE_SCHEME.length());
  226.         }
  227.         return fileName;
  228.     }

  229.     /** The file-based object managed by this handler. */
  230.     private final FileBased content;

  231.     /** A reference to the current {@code FileLocator} object. */
  232.     private final AtomicReference<FileLocator> fileLocator;

  233.     /** A collection with the registered listeners. */
  234.     private final List<FileHandlerListener> listeners = new CopyOnWriteArrayList<>();

  235.     /**
  236.      * Creates a new instance of {@code FileHandler} which is not associated with a {@code FileBased} object and thus does
  237.      * not have a content. Objects of this kind can be used to define a file location, but it is not possible to actually
  238.      * load or save data.
  239.      */
  240.     public FileHandler() {
  241.         this(null);
  242.     }

  243.     /**
  244.      * Creates a new instance of {@code FileHandler} and sets the managed {@code FileBased} object.
  245.      *
  246.      * @param obj the file-based object to manage
  247.      */
  248.     public FileHandler(final FileBased obj) {
  249.         this(obj, emptyFileLocator());
  250.     }

  251.     /**
  252.      * Creates a new instance of {@code FileHandler} which is associated with the given {@code FileBased} object and the
  253.      * location defined for the given {@code FileHandler} object. A copy of the location of the given {@code FileHandler} is
  254.      * created. This constructor is a possibility to associate a file location with a {@code FileBased} object.
  255.      *
  256.      * @param obj the {@code FileBased} object to manage
  257.      * @param c the {@code FileHandler} from which to copy the location (must not be <strong>null</strong>)
  258.      * @throws IllegalArgumentException if the {@code FileHandler} is <strong>null</strong>
  259.      */
  260.     public FileHandler(final FileBased obj, final FileHandler c) {
  261.         this(obj, checkSourceHandler(c).getFileLocator());
  262.     }

  263.     /**
  264.      * Creates a new instance of {@code FileHandler} based on the given {@code FileBased} and {@code FileLocator} objects.
  265.      *
  266.      * @param obj the {@code FileBased} object to manage
  267.      * @param locator the {@code FileLocator}
  268.      */
  269.     private FileHandler(final FileBased obj, final FileLocator locator) {
  270.         content = obj;
  271.         fileLocator = new AtomicReference<>(locator);
  272.     }

  273.     /**
  274.      * Adds a listener to this {@code FileHandler}. It is notified about property changes and IO operations.
  275.      *
  276.      * @param l the listener to be added (must not be <strong>null</strong>)
  277.      * @throws IllegalArgumentException if the listener is <strong>null</strong>
  278.      */
  279.     public void addFileHandlerListener(final FileHandlerListener l) {
  280.         if (l == null) {
  281.             throw new IllegalArgumentException("Listener must not be null!");
  282.         }
  283.         listeners.add(l);
  284.     }

  285.     /**
  286.      * Checks whether a content object is available. If not, an exception is thrown. This method is called whenever the
  287.      * content object is accessed.
  288.      *
  289.      * @throws ConfigurationException if not content object is defined
  290.      */
  291.     private void checkContent() throws ConfigurationException {
  292.         if (getContent() == null) {
  293.             throw new ConfigurationException("No content available!");
  294.         }
  295.     }

  296.     /**
  297.      * Checks whether a content object is available and returns the current {@code FileLocator}. If there is no content
  298.      * object, an exception is thrown. This is a typical operation to be performed before a load() or save() operation.
  299.      *
  300.      * @return the current {@code FileLocator} to be used for the calling operation
  301.      * @throws ConfigurationException if not content object is defined
  302.      */
  303.     private FileLocator checkContentAndGetLocator() throws ConfigurationException {
  304.         checkContent();
  305.         return getFileLocator();
  306.     }

  307.     /**
  308.      * Clears the location of this {@code FileHandler}. Afterwards this handler does not point to any valid file.
  309.      */
  310.     public void clearLocation() {
  311.         new AbstractUpdater() {
  312.             @Override
  313.             protected void updateBuilder(final FileLocatorBuilder builder) {
  314.                 builder.basePath(null).fileName(null).sourceURL(null);
  315.             }
  316.         }.update();
  317.     }

  318.     /**
  319.      * Creates a {@code FileLocator} which is a copy of the passed in one, but has the given file name set to reference the
  320.      * target file.
  321.      *
  322.      * @param fileName the file name
  323.      * @param locator the {@code FileLocator} to copy
  324.      * @return the manipulated {@code FileLocator} with the file name
  325.      */
  326.     private FileLocator createLocatorWithFileName(final String fileName, final FileLocator locator) {
  327.         return FileLocatorUtils.fileLocator(locator).sourceURL(null).fileName(fileName).create();
  328.     }

  329.     /**
  330.      * Obtains a {@code SynchronizerSupport} for the current content. If the content implements this interface, it is
  331.      * returned. Otherwise, result is a dummy object. This method is called before load and save operations. The returned
  332.      * object is used for synchronization.
  333.      *
  334.      * @return the {@code SynchronizerSupport} for synchronization
  335.      */
  336.     private SynchronizerSupport fetchSynchronizerSupport() {
  337.         if (getContent() instanceof SynchronizerSupport) {
  338.             return (SynchronizerSupport) getContent();
  339.         }
  340.         return DUMMY_SYNC_SUPPORT;
  341.     }

  342.     /**
  343.      * Notifies the registered listeners about a completed load operation.
  344.      */
  345.     private void fireLoadedEvent() {
  346.         listeners.forEach(l -> l.loaded(this));
  347.     }

  348.     /**
  349.      * Notifies the registered listeners about the start of a load operation.
  350.      */
  351.     private void fireLoadingEvent() {
  352.         listeners.forEach(l -> l.loading(this));
  353.     }

  354.     /**
  355.      * Notifies the registered listeners about a property update.
  356.      */
  357.     private void fireLocationChangedEvent() {
  358.         listeners.forEach(l -> l.locationChanged(this));
  359.     }

  360.     /**
  361.      * Notifies the registered listeners about a completed save operation.
  362.      */
  363.     private void fireSavedEvent() {
  364.         listeners.forEach(l -> l.saved(this));
  365.     }

  366.     /**
  367.      * Notifies the registered listeners about the start of a save operation.
  368.      */
  369.     private void fireSavingEvent() {
  370.         listeners.forEach(l -> l.saving(this));
  371.     }

  372.     /**
  373.      * Gets the base path. If no base path is defined, but a URL, the base path is derived from there.
  374.      *
  375.      * @return the base path
  376.      */
  377.     public String getBasePath() {
  378.         final FileLocator locator = getFileLocator();
  379.         if (locator.getBasePath() != null) {
  380.             return locator.getBasePath();
  381.         }

  382.         if (locator.getSourceURL() != null) {
  383.             return FileLocatorUtils.getBasePath(locator.getSourceURL());
  384.         }

  385.         return null;
  386.     }

  387.     /**
  388.      * Gets the {@code FileBased} object associated with this {@code FileHandler}.
  389.      *
  390.      * @return the associated {@code FileBased} object
  391.      */
  392.     public final FileBased getContent() {
  393.         return content;
  394.     }

  395.     /**
  396.      * Gets the encoding of the associated file. Result can be <strong>null</strong> if no encoding has been set.
  397.      *
  398.      * @return the encoding of the associated file
  399.      */
  400.     public String getEncoding() {
  401.         return getFileLocator().getEncoding();
  402.     }

  403.     /**
  404.      * Gets the location of the associated file as a {@code File} object. If the base path is a URL with a protocol
  405.      * different than &quot;file&quot;, or the file is within a compressed archive, the return value will not point to a
  406.      * valid file object.
  407.      *
  408.      * @return the location as {@code File} object; this can be <strong>null</strong>
  409.      */
  410.     public File getFile() {
  411.         return createFile(getFileLocator());
  412.     }

  413.     /**
  414.      * Gets a {@code FileLocator} object with the specification of the file stored by this {@code FileHandler}. Note that
  415.      * this method returns the internal data managed by this {@code FileHandler} as it was defined. This is not necessarily
  416.      * the same as the data returned by the single access methods like {@code getFileName()} or {@code getURL()}: These
  417.      * methods try to derive missing data from other values that have been set.
  418.      *
  419.      * @return a {@code FileLocator} with the referenced file
  420.      */
  421.     public FileLocator getFileLocator() {
  422.         return fileLocator.get();
  423.     }

  424.     /**
  425.      * Gets the name of the file. If only a URL is defined, the file name is derived from there.
  426.      *
  427.      * @return the file name
  428.      */
  429.     public String getFileName() {
  430.         final FileLocator locator = getFileLocator();
  431.         if (locator.getFileName() != null) {
  432.             return locator.getFileName();
  433.         }

  434.         if (locator.getSourceURL() != null) {
  435.             return FileLocatorUtils.getFileName(locator.getSourceURL());
  436.         }

  437.         return null;
  438.     }

  439.     /**
  440.      * Gets the {@code FileSystem} to be used by this object when locating files. Result is never <strong>null</strong>; if no file
  441.      * system has been set, the default file system is returned.
  442.      *
  443.      * @return the used {@code FileSystem}
  444.      */
  445.     public FileSystem getFileSystem() {
  446.         return FileLocatorUtils.getFileSystem(getFileLocator());
  447.     }

  448.     /**
  449.      * Gets the {@code FileLocationStrategy} to be applied when accessing the associated file. This method never returns
  450.      * <strong>null</strong>. If a {@code FileLocationStrategy} has been set, it is returned. Otherwise, result is the default
  451.      * {@code FileLocationStrategy}.
  452.      *
  453.      * @return the {@code FileLocationStrategy} to be used
  454.      */
  455.     public FileLocationStrategy getLocationStrategy() {
  456.         return FileLocatorUtils.getLocationStrategy(getFileLocator());
  457.     }

  458.     /**
  459.      * Gets the full path to the associated file. The return value is a valid {@code File} path only if this location is
  460.      * based on a file on the local disk. If the file was loaded from a packed archive, the returned value is the string
  461.      * form of the URL from which the file was loaded.
  462.      *
  463.      * @return the full path to the associated file
  464.      */
  465.     public String getPath() {
  466.         final FileLocator locator = getFileLocator();
  467.         final File file = createFile(locator);
  468.         return FileLocatorUtils.getFileSystem(locator).getPath(file, locator.getSourceURL(), locator.getBasePath(), locator.getFileName());
  469.     }

  470.     /**
  471.      * Gets the location of the associated file as a URL. If a URL is set, it is directly returned. Otherwise, an attempt
  472.      * to locate the referenced file is made.
  473.      *
  474.      * @return a URL to the associated file; can be <strong>null</strong> if the location is unspecified
  475.      */
  476.     public URL getURL() {
  477.         final FileLocator locator = getFileLocator();
  478.         return locator.getSourceURL() != null ? locator.getSourceURL() : FileLocatorUtils.locate(locator);
  479.     }

  480.     /**
  481.      * Injects a {@code FileLocator} pointing to the specified URL if the current {@code FileBased} object implements the
  482.      * {@code FileLocatorAware} interface.
  483.      *
  484.      * @param url the URL for the locator
  485.      */
  486.     private void injectFileLocator(final URL url) {
  487.         if (url == null) {
  488.             injectNullFileLocator();
  489.         } else if (getContent() instanceof FileLocatorAware) {
  490.             final FileLocator locator = prepareNullLocatorBuilder().sourceURL(url).create();
  491.             ((FileLocatorAware) getContent()).initFileLocator(locator);
  492.         }
  493.     }

  494.     /**
  495.      * Checks whether the associated {@code FileBased} object implements the {@code FileLocatorAware} interface. If this is
  496.      * the case, a {@code FileLocator} instance is injected which returns only <strong>null</strong> values. This method is called if
  497.      * no file location is available (for example if data is to be loaded from a stream). The encoding of the injected locator is
  498.      * derived from this object.
  499.      */
  500.     private void injectNullFileLocator() {
  501.         if (getContent() instanceof FileLocatorAware) {
  502.             final FileLocator locator = prepareNullLocatorBuilder().create();
  503.             ((FileLocatorAware) getContent()).initFileLocator(locator);
  504.         }
  505.     }

  506.     /**
  507.      * Tests whether a location is defined for this {@code FileHandler}.
  508.      *
  509.      * @return <strong>true</strong> if a location is defined, <strong>false</strong> otherwise
  510.      */
  511.     public boolean isLocationDefined() {
  512.         return FileLocatorUtils.isLocationDefined(getFileLocator());
  513.     }

  514.     /**
  515.      * Loads the associated file from the underlying location. If no location has been set, an exception is thrown.
  516.      *
  517.      * @throws ConfigurationException if loading of the configuration fails
  518.      */
  519.     public void load() throws ConfigurationException {
  520.         load(checkContentAndGetLocator());
  521.     }

  522.     /**
  523.      * Loads the associated file from the specified {@code File}.
  524.      *
  525.      * @param file the file to load
  526.      * @throws ConfigurationException if an error occurs
  527.      */
  528.     public void load(final File file) throws ConfigurationException {
  529.         final URL url;
  530.         try {
  531.             url = FileLocatorUtils.toURL(file);
  532.         } catch (final MalformedURLException e1) {
  533.             throw new ConfigurationException("Cannot create URL from file " + file);
  534.         }

  535.         load(url);
  536.     }

  537.     /**
  538.      * Internal helper method for loading the associated file from the location specified in the given {@code FileLocator}.
  539.      *
  540.      * @param locator the current {@code FileLocator}
  541.      * @throws ConfigurationException if an error occurs
  542.      */
  543.     private void load(final FileLocator locator) throws ConfigurationException {
  544.         load(FileLocatorUtils.locateOrThrow(locator), locator);
  545.     }

  546.     /**
  547.      * Loads the associated file from the specified stream, using the encoding returned by {@link #getEncoding()}.
  548.      *
  549.      * @param in the input stream
  550.      * @throws ConfigurationException if an error occurs during the load operation
  551.      */
  552.     public void load(final InputStream in) throws ConfigurationException {
  553.         load(in, checkContentAndGetLocator());
  554.     }

  555.     /**
  556.      * Internal helper method for loading a file from the given input stream.
  557.      *
  558.      * @param in the input stream
  559.      * @param locator the current {@code FileLocator}
  560.      * @throws ConfigurationException if an error occurs
  561.      */
  562.     private void load(final InputStream in, final FileLocator locator) throws ConfigurationException {
  563.         load(in, locator.getEncoding());
  564.     }

  565.     /**
  566.      * Loads the associated file from the specified stream, using the specified encoding. If the encoding is <strong>null</strong>,
  567.      * the default encoding is used.
  568.      *
  569.      * @param in the input stream
  570.      * @param encoding the encoding used, {@code null} to use the default encoding
  571.      * @throws ConfigurationException if an error occurs during the load operation
  572.      */
  573.     public void load(final InputStream in, final String encoding) throws ConfigurationException {
  574.         loadFromStream(in, encoding, null);
  575.     }

  576.     /**
  577.      * Loads the associated file from the specified reader.
  578.      *
  579.      * @param in the reader
  580.      * @throws ConfigurationException if an error occurs during the load operation
  581.      */
  582.     public void load(final Reader in) throws ConfigurationException {
  583.         checkContent();
  584.         injectNullFileLocator();
  585.         loadFromReader(in);
  586.     }

  587.     /**
  588.      * Loads the associated file from the given file name. The file name is interpreted in the context of the already set
  589.      * location (for example if it is a relative file name, a base path is applied if available). The underlying location is not
  590.      * changed.
  591.      *
  592.      * @param fileName the name of the file to be loaded
  593.      * @throws ConfigurationException if an error occurs
  594.      */
  595.     public void load(final String fileName) throws ConfigurationException {
  596.         load(fileName, checkContentAndGetLocator());
  597.     }

  598.     /**
  599.      * Internal helper method for loading a file from a file name.
  600.      *
  601.      * @param fileName the file name
  602.      * @param locator the current {@code FileLocator}
  603.      * @throws ConfigurationException if an error occurs
  604.      */
  605.     private void load(final String fileName, final FileLocator locator) throws ConfigurationException {
  606.         final FileLocator locFileName = createLocatorWithFileName(fileName, locator);
  607.         final URL url = FileLocatorUtils.locateOrThrow(locFileName);
  608.         load(url, locator);
  609.     }

  610.     /**
  611.      * Loads the associated file from the specified URL. The location stored in this object is not changed.
  612.      *
  613.      * @param url the URL of the file to be loaded
  614.      * @throws ConfigurationException if an error occurs
  615.      */
  616.     public void load(final URL url) throws ConfigurationException {
  617.         load(url, checkContentAndGetLocator());
  618.     }

  619.     /**
  620.      * Internal helper method for loading a file from the given URL.
  621.      *
  622.      * @param url the URL
  623.      * @param locator the current {@code FileLocator}
  624.      * @throws ConfigurationException if an error occurs
  625.      */
  626.     private void load(final URL url, final FileLocator locator) throws ConfigurationException {
  627.         InputStream in = null;

  628.         try {
  629.             final FileSystem fileSystem = FileLocatorUtils.getFileSystem(locator);
  630.             final URLConnectionOptions urlConnectionOptions = locator.getURLConnectionOptions();
  631.             in = urlConnectionOptions == null ? fileSystem.getInputStream(url) : fileSystem.getInputStream(url, urlConnectionOptions);
  632.             loadFromStream(in, locator.getEncoding(), url);
  633.         } catch (final ConfigurationException e) {
  634.             throw e;
  635.         } catch (final Exception e) {
  636.             throw new ConfigurationException("Unable to load the configuration from the URL " + url, e);
  637.         } finally {
  638.             closeSilent(in);
  639.         }
  640.     }

  641.     /**
  642.      * Internal helper method for loading a file from the given reader.
  643.      *
  644.      * @param in the reader
  645.      * @throws ConfigurationException if an error occurs
  646.      */
  647.     private void loadFromReader(final Reader in) throws ConfigurationException {
  648.         fireLoadingEvent();
  649.         try {
  650.             getContent().read(in);
  651.         } catch (final IOException ioex) {
  652.             throw new ConfigurationException(ioex);
  653.         } finally {
  654.             fireLoadedEvent();
  655.         }
  656.     }

  657.     /**
  658.      * Internal helper method for loading a file from an input stream.
  659.      *
  660.      * @param in the input stream
  661.      * @param encoding the encoding
  662.      * @param url the URL of the file to be loaded (if known)
  663.      * @throws ConfigurationException if an error occurs
  664.      */
  665.     private void loadFromStream(final InputStream in, final String encoding, final URL url) throws ConfigurationException {
  666.         checkContent();
  667.         final SynchronizerSupport syncSupport = fetchSynchronizerSupport();
  668.         syncSupport.lock(LockMode.WRITE);
  669.         try {
  670.             injectFileLocator(url);

  671.             if (getContent() instanceof InputStreamSupport) {
  672.                 loadFromStreamDirectly(in);
  673.             } else {
  674.                 loadFromTransformedStream(in, encoding);
  675.             }
  676.         } finally {
  677.             syncSupport.unlock(LockMode.WRITE);
  678.         }
  679.     }

  680.     /**
  681.      * Loads data from an input stream if the associated {@code FileBased} object implements the {@code InputStreamSupport}
  682.      * interface.
  683.      *
  684.      * @param in the input stream
  685.      * @throws ConfigurationException if an error occurs
  686.      */
  687.     private void loadFromStreamDirectly(final InputStream in) throws ConfigurationException {
  688.         try {
  689.             ((InputStreamSupport) getContent()).read(in);
  690.         } catch (final IOException e) {
  691.             throw new ConfigurationException(e);
  692.         }
  693.     }

  694.     /**
  695.      * Internal helper method for transforming an input stream to a reader and reading its content.
  696.      *
  697.      * @param in the input stream
  698.      * @param encoding the encoding
  699.      * @throws ConfigurationException if an error occurs
  700.      */
  701.     private void loadFromTransformedStream(final InputStream in, final String encoding) throws ConfigurationException {
  702.         Reader reader = null;

  703.         if (encoding != null) {
  704.             try {
  705.                 reader = new InputStreamReader(in, encoding);
  706.             } catch (final UnsupportedEncodingException e) {
  707.                 throw new ConfigurationException("The requested encoding is not supported, try the default encoding.", e);
  708.             }
  709.         }

  710.         if (reader == null) {
  711.             reader = new InputStreamReader(in);
  712.         }

  713.         loadFromReader(reader);
  714.     }

  715.     /**
  716.      * Locates the referenced file if necessary and ensures that the associated {@link FileLocator} is fully initialized.
  717.      * When accessing the referenced file the information stored in the associated {@code FileLocator} is used. If this
  718.      * information is incomplete (for example only the file name is set), an attempt to locate the file may have to be performed on
  719.      * each access. By calling this method such an attempt is performed once, and the results of a successful localization
  720.      * are stored. Hence, later access to the referenced file can be more efficient. Also, all properties pointing to the
  721.      * referenced file in this object's {@code FileLocator} are set (i.e. the URL, the base path, and the file name). If the
  722.      * referenced file cannot be located, result is <strong>false</strong>. This means that the information in the current
  723.      * {@code FileLocator} is insufficient or wrong. If the {@code FileLocator} is already fully defined, it is not changed.
  724.      *
  725.      * @return a flag whether the referenced file could be located successfully
  726.      * @see FileLocatorUtils#fullyInitializedLocator(FileLocator)
  727.      */
  728.     public boolean locate() {
  729.         boolean result;
  730.         boolean done;

  731.         do {
  732.             final FileLocator locator = getFileLocator();
  733.             FileLocator fullLocator = FileLocatorUtils.fullyInitializedLocator(locator);
  734.             if (fullLocator == null) {
  735.                 result = false;
  736.                 fullLocator = locator;
  737.             } else {
  738.                 result = fullLocator != locator || FileLocatorUtils.isFullyInitialized(locator);
  739.             }
  740.             done = fileLocator.compareAndSet(locator, fullLocator);
  741.         } while (!done);

  742.         return result;
  743.     }

  744.     /**
  745.      * Prepares a builder for a {@code FileLocator} which does not have a defined file location. Other properties (for example
  746.      * encoding or file system) are initialized from the {@code FileLocator} associated with this object.
  747.      *
  748.      * @return the initialized builder for a {@code FileLocator}
  749.      */
  750.     private FileLocatorBuilder prepareNullLocatorBuilder() {
  751.         return FileLocatorUtils.fileLocator(getFileLocator()).sourceURL(null).basePath(null).fileName(null);
  752.     }

  753.     /**
  754.      * Removes the specified listener from this object.
  755.      *
  756.      * @param l the listener to be removed
  757.      */
  758.     public void removeFileHandlerListener(final FileHandlerListener l) {
  759.         listeners.remove(l);
  760.     }

  761.     /**
  762.      * Resets the {@code FileSystem} used by this object. It is set to the default file system.
  763.      */
  764.     public void resetFileSystem() {
  765.         setFileSystem(null);
  766.     }

  767.     /**
  768.      * Saves the associated file to the current location set for this object. Before this method can be called a valid
  769.      * location must have been set.
  770.      *
  771.      * @throws ConfigurationException if an error occurs or no location has been set yet
  772.      */
  773.     public void save() throws ConfigurationException {
  774.         save(checkContentAndGetLocator());
  775.     }

  776.     /**
  777.      * Saves the associated file to the specified {@code File}. The file is created automatically if it doesn't exist. This
  778.      * does not change the location of this object (use {@link #setFile} if you need it).
  779.      *
  780.      * @param file the target file
  781.      * @throws ConfigurationException if an error occurs during the save operation
  782.      */
  783.     public void save(final File file) throws ConfigurationException {
  784.         save(file, checkContentAndGetLocator());
  785.     }

  786.     /**
  787.      * Internal helper method for saving data to the given {@code File}.
  788.      *
  789.      * @param file the target file
  790.      * @param locator the current {@code FileLocator}
  791.      * @throws ConfigurationException if an error occurs during the save operation
  792.      */
  793.     private void save(final File file, final FileLocator locator) throws ConfigurationException {
  794.         OutputStream out = null;

  795.         try {
  796.             out = FileLocatorUtils.getFileSystem(locator).getOutputStream(file);
  797.             saveToStream(out, locator.getEncoding(), file.toURI().toURL());
  798.         } catch (final MalformedURLException muex) {
  799.             throw new ConfigurationException(muex);
  800.         } finally {
  801.             closeSilent(out);
  802.         }
  803.     }

  804.     /**
  805.      * Internal helper method for saving data to the internal location stored for this object.
  806.      *
  807.      * @param locator the current {@code FileLocator}
  808.      * @throws ConfigurationException if an error occurs during the save operation
  809.      */
  810.     private void save(final FileLocator locator) throws ConfigurationException {
  811.         if (!FileLocatorUtils.isLocationDefined(locator)) {
  812.             throw new ConfigurationException("No file location has been set!");
  813.         }

  814.         if (locator.getSourceURL() != null) {
  815.             save(locator.getSourceURL(), locator);
  816.         } else {
  817.             save(locator.getFileName(), locator);
  818.         }
  819.     }

  820.     /**
  821.      * Saves the associated file to the specified stream using the encoding returned by {@link #getEncoding()}.
  822.      *
  823.      * @param out the output stream
  824.      * @throws ConfigurationException if an error occurs during the save operation
  825.      */
  826.     public void save(final OutputStream out) throws ConfigurationException {
  827.         save(out, checkContentAndGetLocator());
  828.     }

  829.     /**
  830.      * Internal helper method for saving a file to the given output stream.
  831.      *
  832.      * @param out the output stream
  833.      * @param locator the current {@code FileLocator}
  834.      * @throws ConfigurationException if an error occurs during the save operation
  835.      */
  836.     private void save(final OutputStream out, final FileLocator locator) throws ConfigurationException {
  837.         save(out, locator.getEncoding());
  838.     }

  839.     /**
  840.      * Saves the associated file to the specified stream using the specified encoding. If the encoding is <strong>null</strong>, the
  841.      * default encoding is used.
  842.      *
  843.      * @param out the output stream
  844.      * @param encoding the encoding to be used, {@code null} to use the default encoding
  845.      * @throws ConfigurationException if an error occurs during the save operation
  846.      */
  847.     public void save(final OutputStream out, final String encoding) throws ConfigurationException {
  848.         saveToStream(out, encoding, null);
  849.     }

  850.     /**
  851.      * Saves the associated file to the specified file name. This does not change the location of this object (use
  852.      * {@link #setFileName(String)} if you need it).
  853.      *
  854.      * @param fileName the file name
  855.      * @throws ConfigurationException if an error occurs during the save operation
  856.      */
  857.     public void save(final String fileName) throws ConfigurationException {
  858.         save(fileName, checkContentAndGetLocator());
  859.     }

  860.     /**
  861.      * Internal helper method for saving data to the given file name.
  862.      *
  863.      * @param fileName the path to the target file
  864.      * @param locator the current {@code FileLocator}
  865.      * @throws ConfigurationException if an error occurs during the save operation
  866.      */
  867.     private void save(final String fileName, final FileLocator locator) throws ConfigurationException {
  868.         final URL url;
  869.         try {
  870.             url = FileLocatorUtils.getFileSystem(locator).getURL(locator.getBasePath(), fileName);
  871.         } catch (final MalformedURLException e) {
  872.             throw new ConfigurationException(e);
  873.         }

  874.         if (url == null) {
  875.             throw new ConfigurationException("Cannot locate configuration source " + fileName);
  876.         }
  877.         save(url, locator);
  878.     }

  879.     /**
  880.      * Saves the associated file to the specified URL. This does not change the location of this object (use
  881.      * {@link #setURL(URL)} if you need it).
  882.      *
  883.      * @param url the URL
  884.      * @throws ConfigurationException if an error occurs during the save operation
  885.      */
  886.     public void save(final URL url) throws ConfigurationException {
  887.         save(url, checkContentAndGetLocator());
  888.     }

  889.     /**
  890.      * Internal helper method for saving data to the given URL.
  891.      *
  892.      * @param url the target URL
  893.      * @param locator the {@code FileLocator}
  894.      * @throws ConfigurationException if an error occurs during the save operation
  895.      */
  896.     private void save(final URL url, final FileLocator locator) throws ConfigurationException {
  897.         OutputStream out = null;
  898.         try {
  899.             out = FileLocatorUtils.getFileSystem(locator).getOutputStream(url);
  900.             saveToStream(out, locator.getEncoding(), url);
  901.             if (out instanceof VerifiableOutputStream) {
  902.                 try {
  903.                     ((VerifiableOutputStream) out).verify();
  904.                 } catch (final IOException e) {
  905.                     throw new ConfigurationException(e);
  906.                 }
  907.             }
  908.         } finally {
  909.             closeSilent(out);
  910.         }
  911.     }

  912.     /**
  913.      * Saves the associated file to the given {@code Writer}.
  914.      *
  915.      * @param out the {@code Writer}
  916.      * @throws ConfigurationException if an error occurs during the save operation
  917.      */
  918.     public void save(final Writer out) throws ConfigurationException {
  919.         checkContent();
  920.         injectNullFileLocator();
  921.         saveToWriter(out);
  922.     }

  923.     /**
  924.      * Internal helper method for saving a file to the given stream.
  925.      *
  926.      * @param out the output stream
  927.      * @param encoding the encoding
  928.      * @param url the URL of the output file if known
  929.      * @throws ConfigurationException if an error occurs
  930.      */
  931.     private void saveToStream(final OutputStream out, final String encoding, final URL url) throws ConfigurationException {
  932.         checkContent();
  933.         final SynchronizerSupport syncSupport = fetchSynchronizerSupport();
  934.         syncSupport.lock(LockMode.WRITE);
  935.         try {
  936.             injectFileLocator(url);
  937.             Writer writer = null;

  938.             if (encoding != null) {
  939.                 try {
  940.                     writer = new OutputStreamWriter(out, encoding);
  941.                 } catch (final UnsupportedEncodingException e) {
  942.                     throw new ConfigurationException("The requested encoding is not supported, try the default encoding.", e);
  943.                 }
  944.             }

  945.             if (writer == null) {
  946.                 writer = new OutputStreamWriter(out);
  947.             }

  948.             saveToWriter(writer);
  949.         } finally {
  950.             syncSupport.unlock(LockMode.WRITE);
  951.         }
  952.     }

  953.     /**
  954.      * Internal helper method for saving a file into the given writer.
  955.      *
  956.      * @param out the writer
  957.      * @throws ConfigurationException if an error occurs
  958.      */
  959.     private void saveToWriter(final Writer out) throws ConfigurationException {
  960.         fireSavingEvent();
  961.         try {
  962.             getContent().write(out);
  963.         } catch (final IOException ioex) {
  964.             throw new ConfigurationException(ioex);
  965.         } finally {
  966.             fireSavedEvent();
  967.         }
  968.     }

  969.     /**
  970.      * Sets the base path. The base path is typically either a path to a directory or a URL. Together with the value passed
  971.      * to the {@code setFileName()} method it defines the location of the configuration file to be loaded. The strategies
  972.      * for locating the file are quite tolerant. For instance if the file name is already an absolute path or a fully
  973.      * defined URL, the base path will be ignored. The base path can also be a URL, in which case the file name is
  974.      * interpreted in this URL's context. If other methods are used for determining the location of the associated file
  975.      * (for example {@code setFile()} or {@code setURL()}), the base path is automatically set. Setting the base path using this
  976.      * method automatically sets the URL to <strong>null</strong> because it has to be determined anew based on the file name and the
  977.      * base path.
  978.      *
  979.      * @param basePath the base path.
  980.      */
  981.     public void setBasePath(final String basePath) {
  982.         final String path = normalizeFileURL(basePath);
  983.         new AbstractUpdater() {
  984.             @Override
  985.             protected void updateBuilder(final FileLocatorBuilder builder) {
  986.                 builder.basePath(path);
  987.                 builder.sourceURL(null);
  988.             }
  989.         }.update();
  990.     }

  991.     /**
  992.      * Sets the encoding of the associated file. The encoding applies if binary files are loaded. Note that in this case
  993.      * setting an encoding is recommended; otherwise the platform's default encoding is used.
  994.      *
  995.      * @param encoding the encoding of the associated file
  996.      */
  997.     public void setEncoding(final String encoding) {
  998.         new AbstractUpdater() {
  999.             @Override
  1000.             protected void updateBuilder(final FileLocatorBuilder builder) {
  1001.                 builder.encoding(encoding);
  1002.             }
  1003.         }.update();
  1004.     }

  1005.     /**
  1006.      * Sets the location of the associated file as a {@code File} object. The passed in {@code File} is made absolute if it
  1007.      * is not yet. Then the file's path component becomes the base path and its name component becomes the file name.
  1008.      *
  1009.      * @param file the location of the associated file
  1010.      */
  1011.     public void setFile(final File file) {
  1012.         final String fileName = file.getName();
  1013.         final String basePath = file.getParentFile() != null ? file.getParentFile().getAbsolutePath() : null;
  1014.         new AbstractUpdater() {
  1015.             @Override
  1016.             protected void updateBuilder(final FileLocatorBuilder builder) {
  1017.                 builder.fileName(fileName).basePath(basePath).sourceURL(null);
  1018.             }
  1019.         }.update();
  1020.     }

  1021.     /**
  1022.      * Sets the file to be accessed by this {@code FileHandler} as a {@code FileLocator} object.
  1023.      *
  1024.      * @param locator the {@code FileLocator} with the definition of the file to be accessed (must not be <strong>null</strong>
  1025.      * @throws IllegalArgumentException if the {@code FileLocator} is <strong>null</strong>
  1026.      */
  1027.     public void setFileLocator(final FileLocator locator) {
  1028.         if (locator == null) {
  1029.             throw new IllegalArgumentException("FileLocator must not be null!");
  1030.         }

  1031.         fileLocator.set(locator);
  1032.         fireLocationChangedEvent();
  1033.     }

  1034.     /**
  1035.      * Sets the name of the file. The passed in file name can contain a relative path. It must be used when referring files
  1036.      * with relative paths from classpath. Use {@code setPath()} to set a full qualified file name. The URL is set to
  1037.      * <strong>null</strong> as it has to be determined anew based on the file name and the base path.
  1038.      *
  1039.      * @param fileName the name of the file
  1040.      */
  1041.     public void setFileName(final String fileName) {
  1042.         final String name = normalizeFileURL(fileName);
  1043.         new AbstractUpdater() {
  1044.             @Override
  1045.             protected void updateBuilder(final FileLocatorBuilder builder) {
  1046.                 builder.fileName(name);
  1047.                 builder.sourceURL(null);
  1048.             }
  1049.         }.update();
  1050.     }

  1051.     /**
  1052.      * Sets the {@code FileSystem} to be used by this object when locating files. If a <strong>null</strong> value is passed in, the
  1053.      * file system is reset to the default file system.
  1054.      *
  1055.      * @param fileSystem the {@code FileSystem}
  1056.      */
  1057.     public void setFileSystem(final FileSystem fileSystem) {
  1058.         new AbstractUpdater() {
  1059.             @Override
  1060.             protected void updateBuilder(final FileLocatorBuilder builder) {
  1061.                 builder.fileSystem(fileSystem);
  1062.             }
  1063.         }.update();
  1064.     }

  1065.     /**
  1066.      * Sets the {@code FileLocationStrategy} to be applied when accessing the associated file. The strategy is stored in the
  1067.      * underlying {@link FileLocator}. The argument can be <strong>null</strong>; this causes the default {@code FileLocationStrategy}
  1068.      * to be used.
  1069.      *
  1070.      * @param strategy the {@code FileLocationStrategy}
  1071.      * @see FileLocatorUtils#DEFAULT_LOCATION_STRATEGY
  1072.      */
  1073.     public void setLocationStrategy(final FileLocationStrategy strategy) {
  1074.         new AbstractUpdater() {
  1075.             @Override
  1076.             protected void updateBuilder(final FileLocatorBuilder builder) {
  1077.                 builder.locationStrategy(strategy);
  1078.             }

  1079.         }.update();
  1080.     }

  1081.     /**
  1082.      * Sets the location of the associated file as a full or relative path name. The passed in path should represent a valid
  1083.      * file name on the file system. It must not be used to specify relative paths for files that exist in classpath, either
  1084.      * plain file system or compressed archive, because this method expands any relative path to an absolute one which may
  1085.      * end in an invalid absolute path for classpath references.
  1086.      *
  1087.      * @param path the full path name of the associated file
  1088.      */
  1089.     public void setPath(final String path) {
  1090.         setFile(new File(path));
  1091.     }

  1092.     /**
  1093.      * Sets the location of the associated file as a URL. For loading this can be an arbitrary URL with a supported
  1094.      * protocol. If the file is to be saved, too, a URL with the &quot;file&quot; protocol should be provided. This method
  1095.      * sets the file name and the base path to <strong>null</strong>. They have to be determined anew based on the new URL.
  1096.      *
  1097.      * @param url the location of the file as URL
  1098.      */
  1099.     public void setURL(final URL url) {
  1100.         setURL(url, URLConnectionOptions.DEFAULT);
  1101.     }

  1102.     /**
  1103.      * Sets the location of the associated file as a URL. For loading this can be an arbitrary URL with a supported
  1104.      * protocol. If the file is to be saved, too, a URL with the &quot;file&quot; protocol should be provided. This method
  1105.      * sets the file name and the base path to <strong>null</strong>. They have to be determined anew based on the new URL.
  1106.      *
  1107.      * @param url the location of the file as URL
  1108.      * @param urlConnectionOptions URL connection options
  1109.      * @since 2.8.0
  1110.      */
  1111.     public void setURL(final URL url, final URLConnectionOptions urlConnectionOptions) {
  1112.         new AbstractUpdater() {
  1113.             @Override
  1114.             protected void updateBuilder(final FileLocatorBuilder builder) {
  1115.                 builder.sourceURL(url);
  1116.                 builder.urlConnectionOptions(urlConnectionOptions);
  1117.                 builder.basePath(null).fileName(null);
  1118.             }
  1119.         }.update();
  1120.     }
  1121. }