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