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