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