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