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