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