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