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.File; 020import java.net.MalformedURLException; 021import java.net.URI; 022import java.net.URL; 023import java.util.Arrays; 024import java.util.Map; 025 026import org.apache.commons.configuration2.ex.ConfigurationException; 027import org.apache.commons.lang3.ObjectUtils; 028import org.apache.commons.lang3.StringUtils; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031 032/** 033 * <p> 034 * A utility class providing helper methods related to locating files. 035 * </p> 036 * <p> 037 * The methods of this class are used behind the scenes when retrieving 038 * configuration files based on different criteria, e.g. URLs, files, or more 039 * complex search strategies. They also implement functionality required by the 040 * default {@link FileSystem} implementations. Most methods are intended to be 041 * used internally only by other classes in the {@code io} package. 042 * </p> 043 * 044 * @since 2.0 045 */ 046public final class FileLocatorUtils 047{ 048 /** 049 * Constant for the default {@code FileSystem}. This file system is used by 050 * operations of this class if no specific file system is provided. An 051 * instance of {@link DefaultFileSystem} is used. 052 */ 053 public static final FileSystem DEFAULT_FILE_SYSTEM = 054 new DefaultFileSystem(); 055 056 /** 057 * Constant for the default {@code FileLocationStrategy}. This strategy is 058 * used by the {@code locate()} method if the passed in {@code FileLocator} 059 * does not define its own location strategy. The default location strategy 060 * is roughly equivalent to the search algorithm used in version 1.x of 061 * <em>Commons Configuration</em> (there it was hard-coded though). It 062 * behaves in the following way when passed a {@code FileLocator}: 063 * <ul> 064 * <li>If the {@code FileLocator} has a defined URL, this URL is used as the 065 * file's URL (without any further checks).</li> 066 * <li>Otherwise, base path and file name stored in the {@code FileLocator} 067 * are passed to the current {@code FileSystem}'s {@code locateFromURL()} 068 * method. If this results in a URL, it is returned.</li> 069 * <li>Otherwise, if the locator's file name is an absolute path to an 070 * existing file, the URL of this file is returned.</li> 071 * <li>Otherwise, the concatenation of base path and file name is 072 * constructed. If this path points to an existing file, its URL is 073 * returned.</li> 074 * <li>Otherwise, a sub directory of the current user's home directory as 075 * defined by the base path is searched for the referenced file. If the file 076 * can be found there, its URL is returned.</li> 077 * <li>Otherwise, the base path is ignored, and the file name is searched in 078 * the current user's home directory. If the file can be found there, its 079 * URL is returned.</li> 080 * <li>Otherwise, a resource with the name of the locator's file name is 081 * searched in the classpath. If it can be found, its URL is returned.</li> 082 * <li>Otherwise, the strategy gives up and returns <b>null</b> indicating 083 * that the file cannot be resolved.</li> 084 * </ul> 085 */ 086 public static final FileLocationStrategy DEFAULT_LOCATION_STRATEGY = 087 initDefaultLocationStrategy(); 088 089 /** Constant for the file URL protocol */ 090 private static final String FILE_SCHEME = "file:"; 091 092 /** The logger.*/ 093 private static final Log LOG = LogFactory.getLog(FileLocatorUtils.class); 094 095 /** Property key for the base path. */ 096 private static final String PROP_BASE_PATH = "basePath"; 097 098 /** Property key for the encoding. */ 099 private static final String PROP_ENCODING = "encoding"; 100 101 /** Property key for the file name. */ 102 private static final String PROP_FILE_NAME = "fileName"; 103 104 /** Property key for the file system. */ 105 private static final String PROP_FILE_SYSTEM = "fileSystem"; 106 107 /** Property key for the location strategy. */ 108 private static final String PROP_STRATEGY = "locationStrategy"; 109 110 /** Property key for the source URL. */ 111 private static final String PROP_SOURCE_URL = "sourceURL"; 112 113 /** 114 * Private constructor so that no instances can be created. 115 */ 116 private FileLocatorUtils() 117 { 118 } 119 120 /** 121 * Tries to convert the specified URL to a file object. If this fails, 122 * <b>null</b> is returned. 123 * 124 * @param url the URL 125 * @return the resulting file object 126 */ 127 public static File fileFromURL(final URL url) 128 { 129 return FileUtils.toFile(url); 130 } 131 132 /** 133 * Returns an uninitialized {@code FileLocatorBuilder} which can be used 134 * for the creation of a {@code FileLocator} object. This method provides 135 * a convenient way to create file locators using a fluent API as in the 136 * following example: 137 * <pre> 138 * FileLocator locator = FileLocatorUtils.fileLocator() 139 * .basePath(myBasePath) 140 * .fileName("test.xml") 141 * .create(); 142 * </pre> 143 * @return a builder object for defining a {@code FileLocator} 144 */ 145 public static FileLocator.FileLocatorBuilder fileLocator() 146 { 147 return fileLocator(null); 148 } 149 150 /** 151 * Returns a {@code FileLocatorBuilder} which is already initialized with 152 * the properties of the passed in {@code FileLocator}. This builder can 153 * be used to create a {@code FileLocator} object which shares properties 154 * of the original locator (e.g. the {@code FileSystem} or the encoding), 155 * but points to a different file. An example use case is as follows: 156 * <pre> 157 * FileLocator loc1 = ... 158 * FileLocator loc2 = FileLocatorUtils.fileLocator(loc1) 159 * .setFileName("anotherTest.xml") 160 * .create(); 161 * </pre> 162 * @param src the source {@code FileLocator} (may be <b>null</b>) 163 * @return an initialized builder object for defining a {@code FileLocator} 164 */ 165 public static FileLocator.FileLocatorBuilder fileLocator(final FileLocator src) 166 { 167 return new FileLocator.FileLocatorBuilder(src); 168 } 169 170 /** 171 * Creates a new {@code FileLocator} object with the properties defined in 172 * the given map. The map must be conform to the structure generated by the 173 * {@link #put(FileLocator, Map)} method; unexpected data can cause 174 * {@code ClassCastException} exceptions. The map can be <b>null</b>, then 175 * an uninitialized {@code FileLocator} is returned. 176 * 177 * @param map the map 178 * @return the new {@code FileLocator} 179 * @throws ClassCastException if the map contains invalid data 180 */ 181 public static FileLocator fromMap(final Map<String, ?> map) 182 { 183 final FileLocator.FileLocatorBuilder builder = fileLocator(); 184 if (map != null) 185 { 186 builder.basePath((String) map.get(PROP_BASE_PATH)) 187 .encoding((String) map.get(PROP_ENCODING)) 188 .fileName((String) map.get(PROP_FILE_NAME)) 189 .fileSystem((FileSystem) map.get(PROP_FILE_SYSTEM)) 190 .locationStrategy( 191 (FileLocationStrategy) map.get(PROP_STRATEGY)) 192 .sourceURL((URL) map.get(PROP_SOURCE_URL)); 193 } 194 return builder.create(); 195 } 196 197 /** 198 * Stores the specified {@code FileLocator} in the given map. With the 199 * {@link #fromMap(Map)} method a new {@code FileLocator} with the same 200 * properties as the original one can be created. 201 * 202 * @param locator the {@code FileLocator} to be stored 203 * @param map the map in which to store the {@code FileLocator} (must not be 204 * <b>null</b>) 205 * @throws IllegalArgumentException if the map is <b>null</b> 206 */ 207 public static void put(final FileLocator locator, final Map<String, Object> map) 208 { 209 if (map == null) 210 { 211 throw new IllegalArgumentException("Map must not be null!"); 212 } 213 214 if (locator != null) 215 { 216 map.put(PROP_BASE_PATH, locator.getBasePath()); 217 map.put(PROP_ENCODING, locator.getEncoding()); 218 map.put(PROP_FILE_NAME, locator.getFileName()); 219 map.put(PROP_FILE_SYSTEM, locator.getFileSystem()); 220 map.put(PROP_SOURCE_URL, locator.getSourceURL()); 221 map.put(PROP_STRATEGY, locator.getLocationStrategy()); 222 } 223 } 224 225 /** 226 * Checks whether the specified {@code FileLocator} contains enough 227 * information to locate a file. This is the case if a file name or a URL is 228 * defined. If the passed in {@code FileLocator} is <b>null</b>, result is 229 * <b>false</b>. 230 * 231 * @param locator the {@code FileLocator} to check 232 * @return a flag whether a file location is defined by this 233 * {@code FileLocator} 234 */ 235 public static boolean isLocationDefined(final FileLocator locator) 236 { 237 return locator != null 238 && (locator.getFileName() != null || locator.getSourceURL() != null); 239 } 240 241 /** 242 * Returns a flag whether all components of the given {@code FileLocator} 243 * describing the referenced file are defined. In order to reference a file, 244 * it is not necessary that all components are filled in (for instance, the 245 * URL alone is sufficient). For some use cases however, it might be of 246 * interest to have different methods for accessing the referenced file. 247 * Also, depending on the filled out properties, there is a subtle 248 * difference how the file is accessed: If only the file name is set (and 249 * optionally the base path), each time the file is accessed a 250 * {@code locate()} operation has to be performed to uniquely identify the 251 * file. If however the URL is determined once based on the other components 252 * and stored in a fully defined {@code FileLocator}, it can be used 253 * directly to identify the file. If the passed in {@code FileLocator} is 254 * <b>null</b>, result is <b>false</b>. 255 * 256 * @param locator the {@code FileLocator} to be checked (may be <b>null</b>) 257 * @return a flag whether all components describing the referenced file are 258 * initialized 259 */ 260 public static boolean isFullyInitialized(final FileLocator locator) 261 { 262 if (locator == null) 263 { 264 return false; 265 } 266 return locator.getBasePath() != null && locator.getFileName() != null 267 && locator.getSourceURL() != null; 268 } 269 270 /** 271 * Returns a {@code FileLocator} object based on the passed in one whose 272 * location is fully defined. This method ensures that all components of the 273 * {@code FileLocator} pointing to the file are set in a consistent way. In 274 * detail it behaves as follows: 275 * <ul> 276 * <li>If the {@code FileLocator} has already all components set which 277 * define the file, it is returned unchanged. <em>Note:</em> It is not 278 * checked whether all components are really consistent!</li> 279 * <li>{@link #locate(FileLocator)} is called to determine a unique URL 280 * pointing to the referenced file. If this is successful, a new 281 * {@code FileLocator} is created as a copy of the passed in one, but with 282 * all components pointing to the file derived from this URL.</li> 283 * <li>Otherwise, result is <b>null</b>.</li> 284 * </ul> 285 * 286 * @param locator the {@code FileLocator} to be completed 287 * @return a {@code FileLocator} with a fully initialized location if 288 * possible or <b>null</b> 289 */ 290 public static FileLocator fullyInitializedLocator(final FileLocator locator) 291 { 292 if (isFullyInitialized(locator)) 293 { 294 // already fully initialized 295 return locator; 296 } 297 298 final URL url = locate(locator); 299 return url != null ? createFullyInitializedLocatorFromURL(locator, 300 url) : null; 301 } 302 303 /** 304 * Locates the provided {@code FileLocator}, returning a URL for accessing 305 * the referenced file. This method uses a {@link FileLocationStrategy} to 306 * locate the file the passed in {@code FileLocator} points to. If the 307 * {@code FileLocator} contains itself a {@code FileLocationStrategy}, it is 308 * used. Otherwise, the default {@code FileLocationStrategy} is applied. The 309 * strategy is passed the locator and a {@code FileSystem}. The resulting 310 * URL is returned. If the {@code FileLocator} is <b>null</b>, result is 311 * <b>null</b>. 312 * 313 * @param locator the {@code FileLocator} to be resolved 314 * @return the URL pointing to the referenced file or <b>null</b> if the 315 * {@code FileLocator} could not be resolved 316 * @see #DEFAULT_LOCATION_STRATEGY 317 */ 318 public static URL locate(final FileLocator locator) 319 { 320 if (locator == null) 321 { 322 return null; 323 } 324 325 return obtainLocationStrategy(locator).locate( 326 obtainFileSystem(locator), locator); 327 } 328 329 /** 330 * Tries to locate the file referenced by the passed in {@code FileLocator}. 331 * If this fails, an exception is thrown. This method works like 332 * {@link #locate(FileLocator)}; however, in case of a failed location 333 * attempt an exception is thrown. 334 * 335 * @param locator the {@code FileLocator} to be resolved 336 * @return the URL pointing to the referenced file 337 * @throws ConfigurationException if the file cannot be resolved 338 */ 339 public static URL locateOrThrow(final FileLocator locator) 340 throws ConfigurationException 341 { 342 final URL url = locate(locator); 343 if (url == null) 344 { 345 throw new ConfigurationException("Could not locate: " + locator); 346 } 347 return url; 348 } 349 350 /** 351 * Return the path without the file name, for example http://xyz.net/foo/bar.xml 352 * results in http://xyz.net/foo/ 353 * 354 * @param url the URL from which to extract the path 355 * @return the path component of the passed in URL 356 */ 357 static String getBasePath(final URL url) 358 { 359 if (url == null) 360 { 361 return null; 362 } 363 364 String s = url.toString(); 365 if (s.startsWith(FILE_SCHEME) && !s.startsWith("file://")) 366 { 367 s = "file://" + s.substring(FILE_SCHEME.length()); 368 } 369 370 if (s.endsWith("/") || StringUtils.isEmpty(url.getPath())) 371 { 372 return s; 373 } 374 return s.substring(0, s.lastIndexOf("/") + 1); 375 } 376 377 /** 378 * Extract the file name from the specified URL. 379 * 380 * @param url the URL from which to extract the file name 381 * @return the extracted file name 382 */ 383 static String getFileName(final URL url) 384 { 385 if (url == null) 386 { 387 return null; 388 } 389 390 final String path = url.getPath(); 391 392 if (path.endsWith("/") || StringUtils.isEmpty(path)) 393 { 394 return null; 395 } 396 return path.substring(path.lastIndexOf("/") + 1); 397 } 398 399 /** 400 * Tries to convert the specified base path and file name into a file object. 401 * This method is called e.g. by the save() methods of file based 402 * configurations. The parameter strings can be relative files, absolute 403 * files and URLs as well. This implementation checks first whether the passed in 404 * file name is absolute. If this is the case, it is returned. Otherwise 405 * further checks are performed whether the base path and file name can be 406 * combined to a valid URL or a valid file name. <em>Note:</em> The test 407 * if the passed in file name is absolute is performed using 408 * {@code java.io.File.isAbsolute()}. If the file name starts with a 409 * slash, this method will return <b>true</b> on Unix, but <b>false</b> on 410 * Windows. So to ensure correct behavior for relative file names on all 411 * platforms you should never let relative paths start with a slash. E.g. 412 * in a configuration definition file do not use something like that: 413 * <pre> 414 * <properties fileName="/subdir/my.properties"/> 415 * </pre> 416 * Under Windows this path would be resolved relative to the configuration 417 * definition file. Under Unix this would be treated as an absolute path 418 * name. 419 * 420 * @param basePath the base path 421 * @param fileName the file name (must not be <b>null</b>) 422 * @return the file object (<b>null</b> if no file can be obtained) 423 */ 424 static File getFile(final String basePath, final String fileName) 425 { 426 // Check if the file name is absolute 427 final File f = new File(fileName); 428 if (f.isAbsolute()) 429 { 430 return f; 431 } 432 433 // Check if URLs are involved 434 URL url; 435 try 436 { 437 url = new URL(new URL(basePath), fileName); 438 } 439 catch (final MalformedURLException mex1) 440 { 441 try 442 { 443 url = new URL(fileName); 444 } 445 catch (final MalformedURLException mex2) 446 { 447 url = null; 448 } 449 } 450 451 if (url != null) 452 { 453 return fileFromURL(url); 454 } 455 456 return constructFile(basePath, fileName); 457 } 458 459 /** 460 * Convert the specified file into an URL. This method is equivalent 461 * to file.toURI().toURL(). It was used to work around a bug in the JDK 462 * preventing the transformation of a file into an URL if the file name 463 * contains a '#' character. See the issue CONFIGURATION-300 for 464 * more details. Now that we switched to JDK 1.4 we can directly use 465 * file.toURI().toURL(). 466 * 467 * @param file the file to be converted into an URL 468 */ 469 static URL toURL(final File file) throws MalformedURLException 470 { 471 return file.toURI().toURL(); 472 } 473 474 /** 475 * Tries to convert the specified URI to a URL. If this causes an exception, 476 * result is <b>null</b>. 477 * 478 * @param uri the URI to be converted 479 * @return the resulting URL or <b>null</b> 480 */ 481 static URL convertURIToURL(final URI uri) 482 { 483 try 484 { 485 return uri.toURL(); 486 } 487 catch (final MalformedURLException e) 488 { 489 return null; 490 } 491 } 492 493 /** 494 * Tries to convert the specified file to a URL. If this causes an 495 * exception, result is <b>null</b>. 496 * 497 * @param file the file to be converted 498 * @return the resulting URL or <b>null</b> 499 */ 500 static URL convertFileToURL(final File file) 501 { 502 return convertURIToURL(file.toURI()); 503 } 504 505 /** 506 * Tries to find a resource with the given name in the classpath. 507 * 508 * @param resourceName the name of the resource 509 * @return the URL to the found resource or <b>null</b> if the resource 510 * cannot be found 511 */ 512 static URL locateFromClasspath(final String resourceName) 513 { 514 URL url = null; 515 // attempt to load from the context classpath 516 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 517 if (loader != null) 518 { 519 url = loader.getResource(resourceName); 520 521 if (url != null) 522 { 523 LOG.debug("Loading configuration from the context classpath (" + resourceName + ")"); 524 } 525 } 526 527 // attempt to load from the system classpath 528 if (url == null) 529 { 530 url = ClassLoader.getSystemResource(resourceName); 531 532 if (url != null) 533 { 534 LOG.debug("Loading configuration from the system classpath (" + resourceName + ")"); 535 } 536 } 537 return url; 538 } 539 540 /** 541 * Helper method for constructing a file object from a base path and a 542 * file name. This method is called if the base path passed to 543 * {@code getURL()} does not seem to be a valid URL. 544 * 545 * @param basePath the base path 546 * @param fileName the file name (must not be <b>null</b>) 547 * @return the resulting file 548 */ 549 static File constructFile(final String basePath, final String fileName) 550 { 551 File file; 552 553 final File absolute = new File(fileName); 554 if (StringUtils.isEmpty(basePath) || absolute.isAbsolute()) 555 { 556 file = absolute; 557 } 558 else 559 { 560 file = new File(appendPath(basePath, fileName)); 561 } 562 563 return file; 564 } 565 566 /** 567 * Extends a path by another component. The given extension is added to the 568 * already existing path adding a separator if necessary. 569 * 570 * @param path the path to be extended 571 * @param ext the extension of the path 572 * @return the extended path 573 */ 574 static String appendPath(final String path, final String ext) 575 { 576 final StringBuilder fName = new StringBuilder(); 577 fName.append(path); 578 579 // My best friend. Paranoia. 580 if (!path.endsWith(File.separator)) 581 { 582 fName.append(File.separator); 583 } 584 585 // 586 // We have a relative path, and we have 587 // two possible forms here. If we have the 588 // "./" form then just strip that off first 589 // before continuing. 590 // 591 if (ext.startsWith("." + File.separator)) 592 { 593 fName.append(ext.substring(2)); 594 } 595 else 596 { 597 fName.append(ext); 598 } 599 return fName.toString(); 600 } 601 602 /** 603 * Obtains a non-<b>null</b> {@code FileSystem} object from the passed in 604 * {@code FileLocator}. If the passed in {@code FileLocator} has a 605 * {@code FileSystem} object, it is returned. Otherwise, result is the 606 * default {@code FileSystem}. 607 * 608 * @param locator the {@code FileLocator} (may be <b>null</b>) 609 * @return the {@code FileSystem} to be used for this {@code FileLocator} 610 */ 611 static FileSystem obtainFileSystem(final FileLocator locator) 612 { 613 return locator != null ? ObjectUtils.defaultIfNull( 614 locator.getFileSystem(), DEFAULT_FILE_SYSTEM) 615 : DEFAULT_FILE_SYSTEM; 616 } 617 618 /** 619 * Obtains a non <b>null</b> {@code FileLocationStrategy} object from the 620 * passed in {@code FileLocator}. If the {@code FileLocator} is not 621 * <b>null</b> and has a {@code FileLocationStrategy} defined, this strategy 622 * is returned. Otherwise, result is the default 623 * {@code FileLocationStrategy}. 624 * 625 * @param locator the {@code FileLocator} 626 * @return the {@code FileLocationStrategy} for this {@code FileLocator} 627 */ 628 static FileLocationStrategy obtainLocationStrategy(final FileLocator locator) 629 { 630 return locator != null ? ObjectUtils.defaultIfNull( 631 locator.getLocationStrategy(), DEFAULT_LOCATION_STRATEGY) 632 : DEFAULT_LOCATION_STRATEGY; 633 } 634 635 /** 636 * Creates a fully initialized {@code FileLocator} based on the specified 637 * URL. 638 * 639 * @param src the source {@code FileLocator} 640 * @param url the URL 641 * @return the fully initialized {@code FileLocator} 642 */ 643 private static FileLocator createFullyInitializedLocatorFromURL(final FileLocator src, 644 final URL url) 645 { 646 final FileLocator.FileLocatorBuilder fileLocatorBuilder = fileLocator(src); 647 if (src.getSourceURL() == null) 648 { 649 fileLocatorBuilder.sourceURL(url); 650 } 651 if (StringUtils.isBlank(src.getFileName())) 652 { 653 fileLocatorBuilder.fileName(getFileName(url)); 654 } 655 if (StringUtils.isBlank(src.getBasePath())) 656 { 657 fileLocatorBuilder.basePath(getBasePath(url)); 658 } 659 return fileLocatorBuilder.create(); 660 } 661 662 /** 663 * Creates the default location strategy. This method creates a combined 664 * location strategy as described in the comment of the 665 * {@link #DEFAULT_LOCATION_STRATEGY} member field. 666 * 667 * @return the default {@code FileLocationStrategy} 668 */ 669 private static FileLocationStrategy initDefaultLocationStrategy() 670 { 671 final FileLocationStrategy[] subStrategies = 672 new FileLocationStrategy[] { 673 new ProvidedURLLocationStrategy(), 674 new FileSystemLocationStrategy(), 675 new AbsoluteNameLocationStrategy(), 676 new BasePathLocationStrategy(), 677 new HomeDirectoryLocationStrategy(true), 678 new HomeDirectoryLocationStrategy(false), 679 new ClasspathLocationStrategy() 680 }; 681 return new CombinedLocationStrategy(Arrays.asList(subStrategies)); 682 } 683}