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