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