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