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