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