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