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