View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.io;
18  
19  import java.io.Closeable;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.OutputStream;
25  import java.io.OutputStreamWriter;
26  import java.io.Reader;
27  import java.io.UnsupportedEncodingException;
28  import java.io.Writer;
29  import java.net.MalformedURLException;
30  import java.net.URL;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.concurrent.CopyOnWriteArrayList;
34  import java.util.concurrent.atomic.AtomicReference;
35  
36  import org.apache.commons.configuration2.ex.ConfigurationException;
37  import org.apache.commons.configuration2.io.FileLocator.FileLocatorBuilder;
38  import org.apache.commons.configuration2.sync.LockMode;
39  import org.apache.commons.configuration2.sync.NoOpSynchronizer;
40  import org.apache.commons.configuration2.sync.Synchronizer;
41  import org.apache.commons.configuration2.sync.SynchronizerSupport;
42  import org.apache.commons.logging.LogFactory;
43  
44  /**
45   * <p>
46   * A class that manages persistence of an associated {@link FileBased} object.
47   * </p>
48   * <p>
49   * Instances of this class can be used to load and save arbitrary objects implementing the {@code FileBased} interface
50   * in a convenient way from and to various locations. At construction time the {@code FileBased} object to manage is
51   * passed in. Basically, this object is assigned a location from which it is loaded and to which it can be saved. The
52   * following possibilities exist to specify such a location:
53   * </p>
54   * <ul>
55   * <li>URLs: With the method {@code setURL()} a full URL to the configuration source can be specified. This is the most
56   * flexible way. Note that the {@code save()} methods support only <em>file:</em> URLs.</li>
57   * <li>Files: The {@code setFile()} method allows to specify the configuration source as a file. This can be either a
58   * relative or an absolute file. In the former case the file is resolved based on the current directory.</li>
59   * <li>As file paths in string form: With the {@code setPath()} method a full path to a configuration file can be
60   * provided as a string.</li>
61   * <li>Separated as base path and file name: The base path is a string defining either a local directory or a URL. It
62   * can be set using the {@code setBasePath()} method. The file name, non surprisingly, defines the name of the
63   * configuration file.</li>
64   * </ul>
65   * <p>
66   * An instance stores a location. The {@code load()} and {@code save()} methods that do not take an argument make use of
67   * this internal location. Alternatively, it is also possible to use overloaded variants of {@code load()} and
68   * {@code save()} which expect a location. In these cases the location specified takes precedence over the internal one;
69   * the internal location is not changed.
70   * </p>
71   * <p>
72   * The actual position of the file to be loaded is determined by a {@link FileLocationStrategy} based on the location
73   * information that has been provided. By providing a custom location strategy the algorithm for searching files can be
74   * adapted. Save operations require more explicit information. They cannot rely on a location strategy because the file
75   * to be written may not yet exist. So there may be some differences in the way location information is interpreted by
76   * load and save operations. In order to avoid this, the following approach is recommended:
77   * </p>
78   * <ul>
79   * <li>Use the desired {@code setXXX()} methods to define the location of the file to be loaded.</li>
80   * <li>Call the {@code locate()} method. This method resolves the referenced file (if possible) and fills out all
81   * supported location information.</li>
82   * <li>Later on, {@code save()} can be called. This method now has sufficient information to store the file at the
83   * correct location.</li>
84   * </ul>
85   * <p>
86   * When loading or saving a {@code FileBased} object some additional functionality is performed if the object implements
87   * one of the following interfaces:
88   * </p>
89   * <ul>
90   * <li>{@code FileLocatorAware}: In this case an object with the current file location is injected before the load or
91   * save operation is executed. This is useful for {@code FileBased} objects that depend on their current location, e.g.
92   * to resolve relative path names.</li>
93   * <li>{@code SynchronizerSupport}: If this interface is implemented, load and save operations obtain a write lock on
94   * the {@code FileBased} object before they access it. (In case of a save operation, a read lock would probably be
95   * sufficient, but because of the possible injection of a {@link FileLocator} object it is not allowed to perform
96   * multiple save operations in parallel; therefore, by obtaining a write lock, we are on the safe side.)</li>
97   * </ul>
98   * <p>
99   * This class is thread-safe.
100  * </p>
101  *
102  * @since 2.0
103  */
104 public 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 }