FileLocatorUtils.java

  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. import java.io.File;
  19. import java.net.MalformedURLException;
  20. import java.net.URI;
  21. import java.net.URL;
  22. import java.util.Arrays;
  23. import java.util.Map;

  24. import org.apache.commons.configuration2.ex.ConfigurationException;
  25. import org.apache.commons.lang3.ObjectUtils;
  26. import org.apache.commons.lang3.StringUtils;
  27. import org.apache.commons.logging.Log;
  28. import org.apache.commons.logging.LogFactory;

  29. /**
  30.  * <p>
  31.  * A utility class providing helper methods related to locating files.
  32.  * </p>
  33.  * <p>
  34.  * The methods of this class are used behind the scenes when retrieving configuration files based on different criteria,
  35.  * for example URLs, files, or more complex search strategies. They also implement functionality required by the default
  36.  * {@link FileSystem} implementations. Most methods are intended to be used internally only by other classes in the
  37.  * {@code io} package.
  38.  * </p>
  39.  *
  40.  * @since 2.0
  41.  */
  42. public final class FileLocatorUtils {
  43.     /**
  44.      * Constant for the default {@code FileSystem}. This file system is used by operations of this class if no specific file
  45.      * system is provided. An instance of {@link DefaultFileSystem} is used.
  46.      */
  47.     public static final FileSystem DEFAULT_FILE_SYSTEM = new DefaultFileSystem();

  48.     /**
  49.      * Constant for the default {@code FileLocationStrategy}. This strategy is used by the {@code locate()} method if the
  50.      * passed in {@code FileLocator} does not define its own location strategy. The default location strategy is roughly
  51.      * equivalent to the search algorithm used in version 1.x of <em>Commons Configuration</em> (there it was hard-coded
  52.      * though). It behaves in the following way when passed a {@code FileLocator}:
  53.      * <ul>
  54.      * <li>If the {@code FileLocator} has a defined URL, this URL is used as the file's URL (without any further
  55.      * checks).</li>
  56.      * <li>Otherwise, base path and file name stored in the {@code FileLocator} are passed to the current
  57.      * {@code FileSystem}'s {@code locateFromURL()} method. If this results in a URL, it is returned.</li>
  58.      * <li>Otherwise, if the locator's file name is an absolute path to an existing file, the URL of this file is
  59.      * returned.</li>
  60.      * <li>Otherwise, the concatenation of base path and file name is constructed. If this path points to an existing file,
  61.      * its URL is returned.</li>
  62.      * <li>Otherwise, a sub directory of the current user's home directory as defined by the base path is searched for the
  63.      * referenced file. If the file can be found there, its URL is returned.</li>
  64.      * <li>Otherwise, the base path is ignored, and the file name is searched in the current user's home directory. If the
  65.      * file can be found there, its URL is returned.</li>
  66.      * <li>Otherwise, a resource with the name of the locator's file name is searched in the classpath. If it can be found,
  67.      * its URL is returned.</li>
  68.      * <li>Otherwise, the strategy gives up and returns <strong>null</strong> indicating that the file cannot be resolved.</li>
  69.      * </ul>
  70.      */
  71.     public static final FileLocationStrategy DEFAULT_LOCATION_STRATEGY = initDefaultLocationStrategy();

  72.     /** Constant for the file URL protocol */
  73.     private static final String FILE_SCHEME = "file:";

  74.     /** The logger. */
  75.     private static final Log LOG = LogFactory.getLog(FileLocatorUtils.class);

  76.     /** Property key for the base path. */
  77.     private static final String PROP_BASE_PATH = "basePath";

  78.     /** Property key for the encoding. */
  79.     private static final String PROP_ENCODING = "encoding";

  80.     /** Property key for the file name. */
  81.     private static final String PROP_FILE_NAME = "fileName";

  82.     /** Property key for the file system. */
  83.     private static final String PROP_FILE_SYSTEM = "fileSystem";

  84.     /** Property key for the location strategy. */
  85.     private static final String PROP_STRATEGY = "locationStrategy";

  86.     /** Property key for the source URL. */
  87.     private static final String PROP_SOURCE_URL = "sourceURL";

  88.     /**
  89.      * Extends a path by another component. The given extension is added to the already existing path adding a separator if
  90.      * necessary.
  91.      *
  92.      * @param path the path to be extended
  93.      * @param ext the extension of the path
  94.      * @return the extended path
  95.      */
  96.     static String appendPath(final String path, final String ext) {
  97.         final StringBuilder fName = new StringBuilder();
  98.         fName.append(path);

  99.         // My best friend. Paranoia.
  100.         if (!path.endsWith(File.separator)) {
  101.             fName.append(File.separator);
  102.         }

  103.         //
  104.         // We have a relative path, and we have
  105.         // two possible forms here. If we have the
  106.         // "./" form then just strip that off first
  107.         // before continuing.
  108.         //
  109.         if (ext.startsWith("." + File.separator)) {
  110.             fName.append(ext.substring(2));
  111.         } else {
  112.             fName.append(ext);
  113.         }
  114.         return fName.toString();
  115.     }

  116.     /**
  117.      * Helper method for constructing a file object from a base path and a file name. This method is called if the base path
  118.      * passed to {@code getURL()} does not seem to be a valid URL.
  119.      *
  120.      * @param basePath the base path
  121.      * @param fileName the file name (must not be <strong>null</strong>)
  122.      * @return the resulting file
  123.      */
  124.     static File constructFile(final String basePath, final String fileName) {
  125.         final File file;

  126.         final File absolute = new File(fileName);
  127.         if (StringUtils.isEmpty(basePath) || absolute.isAbsolute()) {
  128.             file = absolute;
  129.         } else {
  130.             file = new File(appendPath(basePath, fileName));
  131.         }

  132.         return file;
  133.     }

  134.     /**
  135.      * Tries to convert the specified file to a URL. If this causes an exception, result is <strong>null</strong>.
  136.      *
  137.      * @param file the file to be converted
  138.      * @return the resulting URL or <strong>null</strong>
  139.      */
  140.     static URL convertFileToURL(final File file) {
  141.         return convertURIToURL(file.toURI());
  142.     }

  143.     /**
  144.      * Tries to convert the specified URI to a URL. If this causes an exception, result is <strong>null</strong>.
  145.      *
  146.      * @param uri the URI to be converted
  147.      * @return the resulting URL or <strong>null</strong>
  148.      */
  149.     static URL convertURIToURL(final URI uri) {
  150.         try {
  151.             return uri.toURL();
  152.         } catch (final MalformedURLException e) {
  153.             return null;
  154.         }
  155.     }

  156.     /**
  157.      * Creates a fully initialized {@code FileLocator} based on the specified URL.
  158.      *
  159.      * @param src the source {@code FileLocator}
  160.      * @param url the URL
  161.      * @return the fully initialized {@code FileLocator}
  162.      */
  163.     private static FileLocator createFullyInitializedLocatorFromURL(final FileLocator src, final URL url) {
  164.         final FileLocator.FileLocatorBuilder fileLocatorBuilder = fileLocator(src);
  165.         if (src.getSourceURL() == null) {
  166.             fileLocatorBuilder.sourceURL(url);
  167.         }
  168.         if (StringUtils.isBlank(src.getFileName())) {
  169.             fileLocatorBuilder.fileName(getFileName(url));
  170.         }
  171.         if (StringUtils.isBlank(src.getBasePath())) {
  172.             fileLocatorBuilder.basePath(getBasePath(url));
  173.         }
  174.         return fileLocatorBuilder.create();
  175.     }

  176.     /**
  177.      * Tries to convert the specified URL to a file object. If this fails, <strong>null</strong> is returned.
  178.      *
  179.      * @param url the URL
  180.      * @return the resulting file object
  181.      */
  182.     public static File fileFromURL(final URL url) {
  183.         return FileUtils.toFile(url);
  184.     }

  185.     /**
  186.      * Returns an uninitialized {@code FileLocatorBuilder} which can be used for the creation of a {@code FileLocator}
  187.      * object. This method provides a convenient way to create file locators using a fluent API as in the following example:
  188.      *
  189.      * <pre>
  190.      * FileLocator locator = FileLocatorUtils.fileLocator().basePath(myBasePath).fileName("test.xml").create();
  191.      * </pre>
  192.      *
  193.      * @return a builder object for defining a {@code FileLocator}
  194.      */
  195.     public static FileLocator.FileLocatorBuilder fileLocator() {
  196.         return fileLocator(null);
  197.     }

  198.     /**
  199.      * Returns a {@code FileLocatorBuilder} which is already initialized with the properties of the passed in
  200.      * {@code FileLocator}. This builder can be used to create a {@code FileLocator} object which shares properties of the
  201.      * original locator (for example the {@code FileSystem} or the encoding), but points to a different file. An example use case
  202.      * is as follows:
  203.      *
  204.      * <pre>
  205.      * FileLocator loc1 = ...
  206.      * FileLocator loc2 = FileLocatorUtils.fileLocator(loc1)
  207.      *     .setFileName("anotherTest.xml")
  208.      *     .create();
  209.      * </pre>
  210.      *
  211.      * @param src the source {@code FileLocator} (may be <strong>null</strong>)
  212.      * @return an initialized builder object for defining a {@code FileLocator}
  213.      */
  214.     public static FileLocator.FileLocatorBuilder fileLocator(final FileLocator src) {
  215.         return new FileLocator.FileLocatorBuilder(src);
  216.     }

  217.     /**
  218.      * Creates a new {@code FileLocator} object with the properties defined in the given map. The map must be conform to the
  219.      * structure generated by the {@link #put(FileLocator, Map)} method; unexpected data can cause
  220.      * {@code ClassCastException} exceptions. The map can be <strong>null</strong>, then an uninitialized {@code FileLocator} is
  221.      * returned.
  222.      *
  223.      * @param map the map
  224.      * @return the new {@code FileLocator}
  225.      * @throws ClassCastException if the map contains invalid data
  226.      */
  227.     public static FileLocator fromMap(final Map<String, ?> map) {
  228.         final FileLocator.FileLocatorBuilder builder = fileLocator();
  229.         if (map != null) {
  230.             builder.basePath((String) map.get(PROP_BASE_PATH)).encoding((String) map.get(PROP_ENCODING)).fileName((String) map.get(PROP_FILE_NAME))
  231.                 .fileSystem((FileSystem) map.get(PROP_FILE_SYSTEM)).locationStrategy((FileLocationStrategy) map.get(PROP_STRATEGY))
  232.                 .sourceURL((URL) map.get(PROP_SOURCE_URL));
  233.         }
  234.         return builder.create();
  235.     }

  236.     /**
  237.      * Returns a {@code FileLocator} object based on the passed in one whose location is fully defined. This method ensures
  238.      * that all components of the {@code FileLocator} pointing to the file are set in a consistent way. In detail it behaves
  239.      * as follows:
  240.      * <ul>
  241.      * <li>If the {@code FileLocator} has already all components set which define the file, it is returned unchanged.
  242.      * <em>Note:</em> It is not checked whether all components are really consistent!</li>
  243.      * <li>{@link #locate(FileLocator)} is called to determine a unique URL pointing to the referenced file. If this is
  244.      * successful, a new {@code FileLocator} is created as a copy of the passed in one, but with all components pointing to
  245.      * the file derived from this URL.</li>
  246.      * <li>Otherwise, result is <strong>null</strong>.</li>
  247.      * </ul>
  248.      *
  249.      * @param locator the {@code FileLocator} to be completed
  250.      * @return a {@code FileLocator} with a fully initialized location if possible or <strong>null</strong>
  251.      */
  252.     public static FileLocator fullyInitializedLocator(final FileLocator locator) {
  253.         if (isFullyInitialized(locator)) {
  254.             // already fully initialized
  255.             return locator;
  256.         }

  257.         final URL url = locate(locator);
  258.         return url != null ? createFullyInitializedLocatorFromURL(locator, url) : null;
  259.     }

  260.     /**
  261.      * Gets the path without the file name, for example https://xyz.net/foo/bar.xml results in https://xyz.net/foo/
  262.      *
  263.      * @param url the URL from which to extract the path
  264.      * @return the path component of the passed in URL
  265.      */
  266.     static String getBasePath(final URL url) {
  267.         if (url == null) {
  268.             return null;
  269.         }

  270.         String s = url.toString();
  271.         if (s.startsWith(FILE_SCHEME) && !s.startsWith("file://")) {
  272.             s = "file://" + s.substring(FILE_SCHEME.length());
  273.         }

  274.         if (s.endsWith("/") || StringUtils.isEmpty(url.getPath())) {
  275.             return s;
  276.         }
  277.         return s.substring(0, s.lastIndexOf("/") + 1);
  278.     }

  279.     /**
  280.      * Tries to find a resource with the given name in the classpath.
  281.      *
  282.      * @param resourceName the name of the resource
  283.      * @return the URL to the found resource or <strong>null</strong> if the resource cannot be found
  284.      */
  285.     static URL getClasspathResource(final String resourceName) {
  286.         URL url = null;
  287.         // attempt to load from the context classpath
  288.         final ClassLoader loader = Thread.currentThread().getContextClassLoader();
  289.         if (loader != null) {
  290.             url = loader.getResource(resourceName);

  291.             if (url != null) {
  292.                 LOG.debug("Loading configuration from the context classpath (" + resourceName + ")");
  293.             }
  294.         }

  295.         // attempt to load from the system classpath
  296.         if (url == null) {
  297.             url = ClassLoader.getSystemResource(resourceName);

  298.             if (url != null) {
  299.                 LOG.debug("Loading configuration from the system classpath (" + resourceName + ")");
  300.             }
  301.         }
  302.         return url;
  303.     }

  304.     /**
  305.      * Tries to convert the specified base path and file name into a file object. This method is called for example by the save()
  306.      * methods of file based configurations. The parameter strings can be relative files, absolute files and URLs as well.
  307.      * This implementation checks first whether the passed in file name is absolute. If this is the case, it is returned.
  308.      * Otherwise further checks are performed whether the base path and file name can be combined to a valid URL or a valid
  309.      * file name. <em>Note:</em> The test if the passed in file name is absolute is performed using
  310.      * {@code java.io.File.isAbsolute()}. If the file name starts with a slash, this method will return <strong>true</strong> on Unix,
  311.      * but <strong>false</strong> on Windows. So to ensure correct behavior for relative file names on all platforms you should never
  312.      * let relative paths start with a slash. E.g. in a configuration definition file do not use something like that:
  313.      *
  314.      * <pre>
  315.      * &lt;properties fileName="/subdir/my.properties"/&gt;
  316.      * </pre>
  317.      *
  318.      * Under Windows this path would be resolved relative to the configuration definition file. Under Unix this would be
  319.      * treated as an absolute path name.
  320.      *
  321.      * @param basePath the base path
  322.      * @param fileName the file name (must not be <strong>null</strong>)
  323.      * @return the file object (<strong>null</strong> if no file can be obtained)
  324.      */
  325.     static File getFile(final String basePath, final String fileName) {
  326.         // Check if the file name is absolute
  327.         final File f = new File(fileName);
  328.         if (f.isAbsolute()) {
  329.             return f;
  330.         }

  331.         // Check if URLs are involved
  332.         URL url;
  333.         try {
  334.             url = new URL(new URL(basePath), fileName);
  335.         } catch (final MalformedURLException mex1) {
  336.             try {
  337.                 url = new URL(fileName);
  338.             } catch (final MalformedURLException mex2) {
  339.                 url = null;
  340.             }
  341.         }

  342.         if (url != null) {
  343.             return fileFromURL(url);
  344.         }

  345.         return constructFile(basePath, fileName);
  346.     }

  347.     /**
  348.      * Extract the file name from the specified URL.
  349.      *
  350.      * @param url the URL from which to extract the file name
  351.      * @return the extracted file name
  352.      */
  353.     static String getFileName(final URL url) {
  354.         if (url == null) {
  355.             return null;
  356.         }

  357.         final String path = url.getPath();

  358.         if (path.endsWith("/") || StringUtils.isEmpty(path)) {
  359.             return null;
  360.         }
  361.         return path.substring(path.lastIndexOf("/") + 1);
  362.     }

  363.     /**
  364.      * Obtains a non-<strong>null</strong> {@code FileSystem} object from the passed in {@code FileLocator}. If the passed in
  365.      * {@code FileLocator} has a {@code FileSystem} object, it is returned. Otherwise, result is the default
  366.      * {@code FileSystem}.
  367.      *
  368.      * @param locator the {@code FileLocator} (may be <strong>null</strong>)
  369.      * @return the {@code FileSystem} to be used for this {@code FileLocator}
  370.      */
  371.     static FileSystem getFileSystem(final FileLocator locator) {
  372.         return locator != null ? ObjectUtils.defaultIfNull(locator.getFileSystem(), DEFAULT_FILE_SYSTEM) : DEFAULT_FILE_SYSTEM;
  373.     }

  374.     /**
  375.      * Gets a non <strong>null</strong> {@code FileLocationStrategy} object from the passed in {@code FileLocator}. If the
  376.      * {@code FileLocator} is not <strong>null</strong> and has a {@code FileLocationStrategy} defined, this strategy is returned.
  377.      * Otherwise, result is the default {@code FileLocationStrategy}.
  378.      *
  379.      * @param locator the {@code FileLocator}
  380.      * @return the {@code FileLocationStrategy} for this {@code FileLocator}
  381.      */
  382.     static FileLocationStrategy getLocationStrategy(final FileLocator locator) {
  383.         return locator != null ? ObjectUtils.defaultIfNull(locator.getLocationStrategy(), DEFAULT_LOCATION_STRATEGY) : DEFAULT_LOCATION_STRATEGY;
  384.     }

  385.     /**
  386.      * Creates the default location strategy. This method creates a combined location strategy as described in the comment
  387.      * of the {@link #DEFAULT_LOCATION_STRATEGY} member field.
  388.      *
  389.      * @return the default {@code FileLocationStrategy}
  390.      */
  391.     private static FileLocationStrategy initDefaultLocationStrategy() {
  392.         final FileLocationStrategy[] subStrategies = {new ProvidedURLLocationStrategy(), new FileSystemLocationStrategy(), new AbsoluteNameLocationStrategy(),
  393.             new BasePathLocationStrategy(), new HomeDirectoryLocationStrategy(true), new HomeDirectoryLocationStrategy(false), new ClasspathLocationStrategy()};
  394.         return new CombinedLocationStrategy(Arrays.asList(subStrategies));
  395.     }

  396.     /**
  397.      * Returns a flag whether all components of the given {@code FileLocator} describing the referenced file are defined. In
  398.      * order to reference a file, it is not necessary that all components are filled in (for instance, the URL alone is
  399.      * sufficient). For some use cases however, it might be of interest to have different methods for accessing the
  400.      * referenced file. Also, depending on the filled out properties, there is a subtle difference how the file is accessed:
  401.      * If only the file name is set (and optionally the base path), each time the file is accessed a {@code locate()}
  402.      * operation has to be performed to uniquely identify the file. If however the URL is determined once based on the other
  403.      * components and stored in a fully defined {@code FileLocator}, it can be used directly to identify the file. If the
  404.      * passed in {@code FileLocator} is <strong>null</strong>, result is <strong>false</strong>.
  405.      *
  406.      * @param locator the {@code FileLocator} to be checked (may be <strong>null</strong>)
  407.      * @return a flag whether all components describing the referenced file are initialized
  408.      */
  409.     public static boolean isFullyInitialized(final FileLocator locator) {
  410.         if (locator == null) {
  411.             return false;
  412.         }
  413.         return locator.getBasePath() != null && locator.getFileName() != null && locator.getSourceURL() != null;
  414.     }

  415.     /**
  416.      * Checks whether the specified {@code FileLocator} contains enough information to locate a file. This is the case if a
  417.      * file name or a URL is defined. If the passed in {@code FileLocator} is <strong>null</strong>, result is <strong>false</strong>.
  418.      *
  419.      * @param locator the {@code FileLocator} to check
  420.      * @return a flag whether a file location is defined by this {@code FileLocator}
  421.      */
  422.     public static boolean isLocationDefined(final FileLocator locator) {
  423.         return locator != null && (locator.getFileName() != null || locator.getSourceURL() != null);
  424.     }

  425.     /**
  426.      * Locates the provided {@code FileLocator}, returning a URL for accessing the referenced file. This method uses a
  427.      * {@link FileLocationStrategy} to locate the file the passed in {@code FileLocator} points to. If the
  428.      * {@code FileLocator} contains itself a {@code FileLocationStrategy}, it is used. Otherwise, the default
  429.      * {@code FileLocationStrategy} is applied. The strategy is passed the locator and a {@code FileSystem}. The resulting
  430.      * URL is returned. If the {@code FileLocator} is <strong>null</strong>, result is <strong>null</strong>.
  431.      *
  432.      * @param locator the {@code FileLocator} to be resolved
  433.      * @return the URL pointing to the referenced file or <strong>null</strong> if the {@code FileLocator} could not be resolved
  434.      * @see #DEFAULT_LOCATION_STRATEGY
  435.      */
  436.     public static URL locate(final FileLocator locator) {
  437.         if (locator == null) {
  438.             return null;
  439.         }

  440.         return getLocationStrategy(locator).locate(getFileSystem(locator), locator);
  441.     }

  442.     /**
  443.      * Tries to locate the file referenced by the passed in {@code FileLocator}. If this fails, an exception is thrown. This
  444.      * method works like {@link #locate(FileLocator)}; however, in case of a failed location attempt an exception is thrown.
  445.      *
  446.      * @param locator the {@code FileLocator} to be resolved
  447.      * @return the URL pointing to the referenced file
  448.      * @throws ConfigurationException if the file cannot be resolved
  449.      */
  450.     public static URL locateOrThrow(final FileLocator locator) throws ConfigurationException {
  451.         final URL url = locate(locator);
  452.         if (url == null) {
  453.             throw new ConfigurationException("Could not locate: " + locator);
  454.         }
  455.         return url;
  456.     }

  457.     /**
  458.      * Stores the specified {@code FileLocator} in the given map. With the {@link #fromMap(Map)} method a new
  459.      * {@code FileLocator} with the same properties as the original one can be created.
  460.      *
  461.      * @param locator the {@code FileLocator} to be stored
  462.      * @param map the map in which to store the {@code FileLocator} (must not be <strong>null</strong>)
  463.      * @throws IllegalArgumentException if the map is <strong>null</strong>
  464.      */
  465.     public static void put(final FileLocator locator, final Map<String, Object> map) {
  466.         if (map == null) {
  467.             throw new IllegalArgumentException("Map must not be null!");
  468.         }

  469.         if (locator != null) {
  470.             map.put(PROP_BASE_PATH, locator.getBasePath());
  471.             map.put(PROP_ENCODING, locator.getEncoding());
  472.             map.put(PROP_FILE_NAME, locator.getFileName());
  473.             map.put(PROP_FILE_SYSTEM, locator.getFileSystem());
  474.             map.put(PROP_SOURCE_URL, locator.getSourceURL());
  475.             map.put(PROP_STRATEGY, locator.getLocationStrategy());
  476.         }
  477.     }

  478.     /**
  479.      * Convert the specified file into an URL. This method is equivalent to file.toURI().toURL(). It was used to work around
  480.      * a bug in the JDK preventing the transformation of a file into an URL if the file name contains a '#' character. See
  481.      * the issue CONFIGURATION-300 for more details. Now that we switched to JDK 1.4 we can directly use
  482.      * file.toURI().toURL().
  483.      *
  484.      * @param file the file to be converted into an URL
  485.      * @return a URL
  486.      * @throws MalformedURLException If the file protocol handler is not found (should not happen) or if an error occurred
  487.      *         while constructing the URL
  488.      */
  489.     static URL toURL(final File file) throws MalformedURLException {
  490.         return file.toURI().toURL();
  491.     }

  492.     /**
  493.      * Private constructor so that no instances can be created.
  494.      */
  495.     private FileLocatorUtils() {
  496.     }

  497. }