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