View Javadoc
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      * &lt;properties fileName="/subdir/my.properties"/&gt;
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 }