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