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