001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2.io;
018
019import java.io.File;
020import java.net.MalformedURLException;
021import java.net.URI;
022import java.net.URL;
023import java.util.Arrays;
024import java.util.Map;
025
026import org.apache.commons.configuration2.ex.ConfigurationException;
027import org.apache.commons.lang3.ObjectUtils;
028import org.apache.commons.lang3.StringUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031
032/**
033 * <p>
034 * A utility class providing helper methods related to locating files.
035 * </p>
036 * <p>
037 * The methods of this class are used behind the scenes when retrieving configuration files based on different criteria,
038 * e.g. 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     * Constant for the default {@code FileSystem}. This file system is used by operations of this class if no specific file
048     * system is provided. An instance of {@link DefaultFileSystem} is used.
049     */
050    public static final FileSystem DEFAULT_FILE_SYSTEM = new DefaultFileSystem();
051
052    /**
053     * Constant for the default {@code FileLocationStrategy}. This strategy is used by the {@code locate()} method if the
054     * passed in {@code FileLocator} does not define its own location strategy. The default location strategy is roughly
055     * equivalent to the search algorithm used in version 1.x of <em>Commons Configuration</em> (there it was hard-coded
056     * though). It behaves in the following way when passed a {@code FileLocator}:
057     * <ul>
058     * <li>If the {@code FileLocator} has a defined URL, this URL is used as the file's URL (without any further
059     * checks).</li>
060     * <li>Otherwise, base path and file name stored in the {@code FileLocator} are passed to the current
061     * {@code FileSystem}'s {@code locateFromURL()} method. If this results in a URL, it is returned.</li>
062     * <li>Otherwise, if the locator's file name is an absolute path to an existing file, the URL of this file is
063     * returned.</li>
064     * <li>Otherwise, the concatenation of base path and file name is constructed. If this path points to an existing file,
065     * its URL is returned.</li>
066     * <li>Otherwise, a sub directory of the current user's home directory as defined by the base path is searched for the
067     * referenced file. If the file can be found there, its URL is returned.</li>
068     * <li>Otherwise, the base path is ignored, and the file name is searched in the current user's home directory. If the
069     * file can be found there, its URL is returned.</li>
070     * <li>Otherwise, a resource with the name of the locator's file name is searched in the classpath. If it can be found,
071     * its URL is returned.</li>
072     * <li>Otherwise, the strategy gives up and returns <b>null</b> indicating that the file cannot be resolved.</li>
073     * </ul>
074     */
075    public static final FileLocationStrategy DEFAULT_LOCATION_STRATEGY = initDefaultLocationStrategy();
076
077    /** Constant for the file URL protocol */
078    private static final String FILE_SCHEME = "file:";
079
080    /** The logger. */
081    private static final Log LOG = LogFactory.getLog(FileLocatorUtils.class);
082
083    /** Property key for the base path. */
084    private static final String PROP_BASE_PATH = "basePath";
085
086    /** Property key for the encoding. */
087    private static final String PROP_ENCODING = "encoding";
088
089    /** Property key for the file name. */
090    private static final String PROP_FILE_NAME = "fileName";
091
092    /** Property key for the file system. */
093    private static final String PROP_FILE_SYSTEM = "fileSystem";
094
095    /** Property key for the location strategy. */
096    private static final String PROP_STRATEGY = "locationStrategy";
097
098    /** Property key for the source URL. */
099    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 <b>null</b>)
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 <b>null</b>.
155     *
156     * @param file the file to be converted
157     * @return the resulting URL or <b>null</b>
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 <b>null</b>.
165     *
166     * @param uri the URI to be converted
167     * @return the resulting URL or <b>null</b>
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, <b>null</b> 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 (e.g. 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 <b>null</b>)
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 <b>null</b>, 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 <b>null</b>.</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 <b>null</b>
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 <b>null</b> 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 e.g. 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 <b>true</b> on Unix,
346     * but <b>false</b> 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     * &lt;properties fileName="/subdir/my.properties"/&gt;
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 <b>null</b>)
358     * @return the file object (<b>null</b> 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-<b>null</b> {@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 <b>null</b>)
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 <b>null</b> {@code FileLocationStrategy} object from the passed in {@code FileLocator}. If the
419     * {@code FileLocator} is not <b>null</b> 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 <b>null</b>, result is <b>false</b>.
450     *
451     * @param locator the {@code FileLocator} to be checked (may be <b>null</b>)
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 <b>null</b>, result is <b>false</b>.
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 <b>null</b>, result is <b>null</b>.
478     *
479     * @param locator the {@code FileLocator} to be resolved
480     * @return the URL pointing to the referenced file or <b>null</b> 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 <b>null</b>)
513     * @throws IllegalArgumentException if the map is <b>null</b>
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}