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