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