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