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