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