blob: e06b60f7847c74149c89f6918defb75396d0608a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.configuration2.io;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* <p>
* A utility class providing helper methods related to locating files.
* </p>
* <p>
* The methods of this class are used behind the scenes when retrieving
* configuration files based on different criteria, e.g. URLs, files, or more
* complex search strategies. They also implement functionality required by the
* default {@link FileSystem} implementations. Most methods are intended to be
* used internally only by other classes in the {@code io} package.
* </p>
*
* @since 2.0
*/
public final class FileLocatorUtils
{
/**
* Constant for the default {@code FileSystem}. This file system is used by
* operations of this class if no specific file system is provided. An
* instance of {@link DefaultFileSystem} is used.
*/
public static final FileSystem DEFAULT_FILE_SYSTEM =
new DefaultFileSystem();
/**
* Constant for the default {@code FileLocationStrategy}. This strategy is
* used by the {@code locate()} method if the passed in {@code FileLocator}
* does not define its own location strategy. The default location strategy
* is roughly equivalent to the search algorithm used in version 1.x of
* <em>Commons Configuration</em> (there it was hard-coded though). It
* behaves in the following way when passed a {@code FileLocator}:
* <ul>
* <li>If the {@code FileLocator} has a defined URL, this URL is used as the
* file's URL (without any further checks).</li>
* <li>Otherwise, base path and file name stored in the {@code FileLocator}
* are passed to the current {@code FileSystem}'s {@code locateFromURL()}
* method. If this results in a URL, it is returned.</li>
* <li>Otherwise, if the locator's file name is an absolute path to an
* existing file, the URL of this file is returned.</li>
* <li>Otherwise, the concatenation of base path and file name is
* constructed. If this path points to an existing file, its URL is
* returned.</li>
* <li>Otherwise, a sub directory of the current user's home directory as
* defined by the base path is searched for the referenced file. If the file
* can be found there, its URL is returned.</li>
* <li>Otherwise, the base path is ignored, and the file name is searched in
* the current user's home directory. If the file can be found there, its
* URL is returned.</li>
* <li>Otherwise, a resource with the name of the locator's file name is
* searched in the classpath. If it can be found, its URL is returned.</li>
* <li>Otherwise, the strategy gives up and returns <b>null</b> indicating
* that the file cannot be resolved.</li>
* </ul>
*/
public static final FileLocationStrategy DEFAULT_LOCATION_STRATEGY =
initDefaultLocationStrategy();
/** Constant for the file URL protocol */
private static final String FILE_SCHEME = "file:";
/** The logger.*/
private static final Log LOG = LogFactory.getLog(FileLocatorUtils.class);
/** Property key for the base path. */
private static final String PROP_BASE_PATH = "basePath";
/** Property key for the encoding. */
private static final String PROP_ENCODING = "encoding";
/** Property key for the file name. */
private static final String PROP_FILE_NAME = "fileName";
/** Property key for the file system. */
private static final String PROP_FILE_SYSTEM = "fileSystem";
/** Property key for the location strategy. */
private static final String PROP_STRATEGY = "locationStrategy";
/** Property key for the source URL. */
private static final String PROP_SOURCE_URL = "sourceURL";
/**
* Extends a path by another component. The given extension is added to the
* already existing path adding a separator if necessary.
*
* @param path the path to be extended
* @param ext the extension of the path
* @return the extended path
*/
static String appendPath(final String path, final String ext)
{
final StringBuilder fName = new StringBuilder();
fName.append(path);
// My best friend. Paranoia.
if (!path.endsWith(File.separator))
{
fName.append(File.separator);
}
//
// We have a relative path, and we have
// two possible forms here. If we have the
// "./" form then just strip that off first
// before continuing.
//
if (ext.startsWith("." + File.separator))
{
fName.append(ext.substring(2));
}
else
{
fName.append(ext);
}
return fName.toString();
}
/**
* Helper method for constructing a file object from a base path and a
* file name. This method is called if the base path passed to
* {@code getURL()} does not seem to be a valid URL.
*
* @param basePath the base path
* @param fileName the file name (must not be <b>null</b>)
* @return the resulting file
*/
static File constructFile(final String basePath, final String fileName)
{
File file;
final File absolute = new File(fileName);
if (StringUtils.isEmpty(basePath) || absolute.isAbsolute())
{
file = absolute;
}
else
{
file = new File(appendPath(basePath, fileName));
}
return file;
}
/**
* Tries to convert the specified file to a URL. If this causes an
* exception, result is <b>null</b>.
*
* @param file the file to be converted
* @return the resulting URL or <b>null</b>
*/
static URL convertFileToURL(final File file)
{
return convertURIToURL(file.toURI());
}
/**
* Tries to convert the specified URI to a URL. If this causes an exception,
* result is <b>null</b>.
*
* @param uri the URI to be converted
* @return the resulting URL or <b>null</b>
*/
static URL convertURIToURL(final URI uri)
{
try
{
return uri.toURL();
}
catch (final MalformedURLException e)
{
return null;
}
}
/**
* Creates a fully initialized {@code FileLocator} based on the specified
* URL.
*
* @param src the source {@code FileLocator}
* @param url the URL
* @return the fully initialized {@code FileLocator}
*/
private static FileLocator createFullyInitializedLocatorFromURL(final FileLocator src,
final URL url)
{
final FileLocator.FileLocatorBuilder fileLocatorBuilder = fileLocator(src);
if (src.getSourceURL() == null)
{
fileLocatorBuilder.sourceURL(url);
}
if (StringUtils.isBlank(src.getFileName()))
{
fileLocatorBuilder.fileName(getFileName(url));
}
if (StringUtils.isBlank(src.getBasePath()))
{
fileLocatorBuilder.basePath(getBasePath(url));
}
return fileLocatorBuilder.create();
}
/**
* Tries to convert the specified URL to a file object. If this fails,
* <b>null</b> is returned.
*
* @param url the URL
* @return the resulting file object
*/
public static File fileFromURL(final URL url)
{
return FileUtils.toFile(url);
}
/**
* Returns an uninitialized {@code FileLocatorBuilder} which can be used
* for the creation of a {@code FileLocator} object. This method provides
* a convenient way to create file locators using a fluent API as in the
* following example:
* <pre>
* FileLocator locator = FileLocatorUtils.fileLocator()
* .basePath(myBasePath)
* .fileName("test.xml")
* .create();
* </pre>
* @return a builder object for defining a {@code FileLocator}
*/
public static FileLocator.FileLocatorBuilder fileLocator()
{
return fileLocator(null);
}
/**
* Returns a {@code FileLocatorBuilder} which is already initialized with
* the properties of the passed in {@code FileLocator}. This builder can
* be used to create a {@code FileLocator} object which shares properties
* of the original locator (e.g. the {@code FileSystem} or the encoding),
* but points to a different file. An example use case is as follows:
* <pre>
* FileLocator loc1 = ...
* FileLocator loc2 = FileLocatorUtils.fileLocator(loc1)
* .setFileName("anotherTest.xml")
* .create();
* </pre>
* @param src the source {@code FileLocator} (may be <b>null</b>)
* @return an initialized builder object for defining a {@code FileLocator}
*/
public static FileLocator.FileLocatorBuilder fileLocator(final FileLocator src)
{
return new FileLocator.FileLocatorBuilder(src);
}
/**
* Creates a new {@code FileLocator} object with the properties defined in
* the given map. The map must be conform to the structure generated by the
* {@link #put(FileLocator, Map)} method; unexpected data can cause
* {@code ClassCastException} exceptions. The map can be <b>null</b>, then
* an uninitialized {@code FileLocator} is returned.
*
* @param map the map
* @return the new {@code FileLocator}
* @throws ClassCastException if the map contains invalid data
*/
public static FileLocator fromMap(final Map<String, ?> map)
{
final FileLocator.FileLocatorBuilder builder = fileLocator();
if (map != null)
{
builder.basePath((String) map.get(PROP_BASE_PATH))
.encoding((String) map.get(PROP_ENCODING))
.fileName((String) map.get(PROP_FILE_NAME))
.fileSystem((FileSystem) map.get(PROP_FILE_SYSTEM))
.locationStrategy(
(FileLocationStrategy) map.get(PROP_STRATEGY))
.sourceURL((URL) map.get(PROP_SOURCE_URL));
}
return builder.create();
}
/**
* Returns a {@code FileLocator} object based on the passed in one whose
* location is fully defined. This method ensures that all components of the
* {@code FileLocator} pointing to the file are set in a consistent way. In
* detail it behaves as follows:
* <ul>
* <li>If the {@code FileLocator} has already all components set which
* define the file, it is returned unchanged. <em>Note:</em> It is not
* checked whether all components are really consistent!</li>
* <li>{@link #locate(FileLocator)} is called to determine a unique URL
* pointing to the referenced file. If this is successful, a new
* {@code FileLocator} is created as a copy of the passed in one, but with
* all components pointing to the file derived from this URL.</li>
* <li>Otherwise, result is <b>null</b>.</li>
* </ul>
*
* @param locator the {@code FileLocator} to be completed
* @return a {@code FileLocator} with a fully initialized location if
* possible or <b>null</b>
*/
public static FileLocator fullyInitializedLocator(final FileLocator locator)
{
if (isFullyInitialized(locator))
{
// already fully initialized
return locator;
}
final URL url = locate(locator);
return url != null ? createFullyInitializedLocatorFromURL(locator,
url) : null;
}
/**
* Return the path without the file name, for example http://xyz.net/foo/bar.xml
* results in http://xyz.net/foo/
*
* @param url the URL from which to extract the path
* @return the path component of the passed in URL
*/
static String getBasePath(final URL url)
{
if (url == null)
{
return null;
}
String s = url.toString();
if (s.startsWith(FILE_SCHEME) && !s.startsWith("file://"))
{
s = "file://" + s.substring(FILE_SCHEME.length());
}
if (s.endsWith("/") || StringUtils.isEmpty(url.getPath()))
{
return s;
}
return s.substring(0, s.lastIndexOf("/") + 1);
}
/**
* Tries to convert the specified base path and file name into a file object.
* This method is called e.g. by the save() methods of file based
* configurations. The parameter strings can be relative files, absolute
* files and URLs as well. This implementation checks first whether the passed in
* file name is absolute. If this is the case, it is returned. Otherwise
* further checks are performed whether the base path and file name can be
* combined to a valid URL or a valid file name. <em>Note:</em> The test
* if the passed in file name is absolute is performed using
* {@code java.io.File.isAbsolute()}. If the file name starts with a
* slash, this method will return <b>true</b> on Unix, but <b>false</b> on
* Windows. So to ensure correct behavior for relative file names on all
* platforms you should never let relative paths start with a slash. E.g.
* in a configuration definition file do not use something like that:
* <pre>
* &lt;properties fileName="/subdir/my.properties"/&gt;
* </pre>
* Under Windows this path would be resolved relative to the configuration
* definition file. Under Unix this would be treated as an absolute path
* name.
*
* @param basePath the base path
* @param fileName the file name (must not be <b>null</b>)
* @return the file object (<b>null</b> if no file can be obtained)
*/
static File getFile(final String basePath, final String fileName)
{
// Check if the file name is absolute
final File f = new File(fileName);
if (f.isAbsolute())
{
return f;
}
// Check if URLs are involved
URL url;
try
{
url = new URL(new URL(basePath), fileName);
}
catch (final MalformedURLException mex1)
{
try
{
url = new URL(fileName);
}
catch (final MalformedURLException mex2)
{
url = null;
}
}
if (url != null)
{
return fileFromURL(url);
}
return constructFile(basePath, fileName);
}
/**
* Extract the file name from the specified URL.
*
* @param url the URL from which to extract the file name
* @return the extracted file name
*/
static String getFileName(final URL url)
{
if (url == null)
{
return null;
}
final String path = url.getPath();
if (path.endsWith("/") || StringUtils.isEmpty(path))
{
return null;
}
return path.substring(path.lastIndexOf("/") + 1);
}
/**
* Creates the default location strategy. This method creates a combined
* location strategy as described in the comment of the
* {@link #DEFAULT_LOCATION_STRATEGY} member field.
*
* @return the default {@code FileLocationStrategy}
*/
private static FileLocationStrategy initDefaultLocationStrategy()
{
final FileLocationStrategy[] subStrategies =
new FileLocationStrategy[] {
new ProvidedURLLocationStrategy(),
new FileSystemLocationStrategy(),
new AbsoluteNameLocationStrategy(),
new BasePathLocationStrategy(),
new HomeDirectoryLocationStrategy(true),
new HomeDirectoryLocationStrategy(false),
new ClasspathLocationStrategy()
};
return new CombinedLocationStrategy(Arrays.asList(subStrategies));
}
/**
* Returns a flag whether all components of the given {@code FileLocator}
* describing the referenced file are defined. In order to reference a file,
* it is not necessary that all components are filled in (for instance, the
* URL alone is sufficient). For some use cases however, it might be of
* interest to have different methods for accessing the referenced file.
* Also, depending on the filled out properties, there is a subtle
* difference how the file is accessed: If only the file name is set (and
* optionally the base path), each time the file is accessed a
* {@code locate()} operation has to be performed to uniquely identify the
* file. If however the URL is determined once based on the other components
* and stored in a fully defined {@code FileLocator}, it can be used
* directly to identify the file. If the passed in {@code FileLocator} is
* <b>null</b>, result is <b>false</b>.
*
* @param locator the {@code FileLocator} to be checked (may be <b>null</b>)
* @return a flag whether all components describing the referenced file are
* initialized
*/
public static boolean isFullyInitialized(final FileLocator locator)
{
if (locator == null)
{
return false;
}
return locator.getBasePath() != null && locator.getFileName() != null
&& locator.getSourceURL() != null;
}
/**
* Checks whether the specified {@code FileLocator} contains enough
* information to locate a file. This is the case if a file name or a URL is
* defined. If the passed in {@code FileLocator} is <b>null</b>, result is
* <b>false</b>.
*
* @param locator the {@code FileLocator} to check
* @return a flag whether a file location is defined by this
* {@code FileLocator}
*/
public static boolean isLocationDefined(final FileLocator locator)
{
return locator != null
&& (locator.getFileName() != null || locator.getSourceURL() != null);
}
/**
* Locates the provided {@code FileLocator}, returning a URL for accessing
* the referenced file. This method uses a {@link FileLocationStrategy} to
* locate the file the passed in {@code FileLocator} points to. If the
* {@code FileLocator} contains itself a {@code FileLocationStrategy}, it is
* used. Otherwise, the default {@code FileLocationStrategy} is applied. The
* strategy is passed the locator and a {@code FileSystem}. The resulting
* URL is returned. If the {@code FileLocator} is <b>null</b>, result is
* <b>null</b>.
*
* @param locator the {@code FileLocator} to be resolved
* @return the URL pointing to the referenced file or <b>null</b> if the
* {@code FileLocator} could not be resolved
* @see #DEFAULT_LOCATION_STRATEGY
*/
public static URL locate(final FileLocator locator)
{
if (locator == null)
{
return null;
}
return obtainLocationStrategy(locator).locate(
obtainFileSystem(locator), locator);
}
/**
* Tries to find a resource with the given name in the classpath.
*
* @param resourceName the name of the resource
* @return the URL to the found resource or <b>null</b> if the resource
* cannot be found
*/
static URL locateFromClasspath(final String resourceName)
{
URL url = null;
// attempt to load from the context classpath
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader != null)
{
url = loader.getResource(resourceName);
if (url != null)
{
LOG.debug("Loading configuration from the context classpath (" + resourceName + ")");
}
}
// attempt to load from the system classpath
if (url == null)
{
url = ClassLoader.getSystemResource(resourceName);
if (url != null)
{
LOG.debug("Loading configuration from the system classpath (" + resourceName + ")");
}
}
return url;
}
/**
* Tries to locate the file referenced by the passed in {@code FileLocator}.
* If this fails, an exception is thrown. This method works like
* {@link #locate(FileLocator)}; however, in case of a failed location
* attempt an exception is thrown.
*
* @param locator the {@code FileLocator} to be resolved
* @return the URL pointing to the referenced file
* @throws ConfigurationException if the file cannot be resolved
*/
public static URL locateOrThrow(final FileLocator locator)
throws ConfigurationException
{
final URL url = locate(locator);
if (url == null)
{
throw new ConfigurationException("Could not locate: " + locator);
}
return url;
}
/**
* Obtains a non-<b>null</b> {@code FileSystem} object from the passed in
* {@code FileLocator}. If the passed in {@code FileLocator} has a
* {@code FileSystem} object, it is returned. Otherwise, result is the
* default {@code FileSystem}.
*
* @param locator the {@code FileLocator} (may be <b>null</b>)
* @return the {@code FileSystem} to be used for this {@code FileLocator}
*/
static FileSystem obtainFileSystem(final FileLocator locator)
{
return locator != null ? ObjectUtils.defaultIfNull(
locator.getFileSystem(), DEFAULT_FILE_SYSTEM)
: DEFAULT_FILE_SYSTEM;
}
/**
* Obtains a non <b>null</b> {@code FileLocationStrategy} object from the
* passed in {@code FileLocator}. If the {@code FileLocator} is not
* <b>null</b> and has a {@code FileLocationStrategy} defined, this strategy
* is returned. Otherwise, result is the default
* {@code FileLocationStrategy}.
*
* @param locator the {@code FileLocator}
* @return the {@code FileLocationStrategy} for this {@code FileLocator}
*/
static FileLocationStrategy obtainLocationStrategy(final FileLocator locator)
{
return locator != null ? ObjectUtils.defaultIfNull(
locator.getLocationStrategy(), DEFAULT_LOCATION_STRATEGY)
: DEFAULT_LOCATION_STRATEGY;
}
/**
* Stores the specified {@code FileLocator} in the given map. With the
* {@link #fromMap(Map)} method a new {@code FileLocator} with the same
* properties as the original one can be created.
*
* @param locator the {@code FileLocator} to be stored
* @param map the map in which to store the {@code FileLocator} (must not be
* <b>null</b>)
* @throws IllegalArgumentException if the map is <b>null</b>
*/
public static void put(final FileLocator locator, final Map<String, Object> map)
{
if (map == null)
{
throw new IllegalArgumentException("Map must not be null!");
}
if (locator != null)
{
map.put(PROP_BASE_PATH, locator.getBasePath());
map.put(PROP_ENCODING, locator.getEncoding());
map.put(PROP_FILE_NAME, locator.getFileName());
map.put(PROP_FILE_SYSTEM, locator.getFileSystem());
map.put(PROP_SOURCE_URL, locator.getSourceURL());
map.put(PROP_STRATEGY, locator.getLocationStrategy());
}
}
/**
* Convert the specified file into an URL. This method is equivalent
* to file.toURI().toURL(). It was used to work around a bug in the JDK
* preventing the transformation of a file into an URL if the file name
* contains a '#' character. See the issue CONFIGURATION-300 for
* more details. Now that we switched to JDK 1.4 we can directly use
* file.toURI().toURL().
*
* @param file the file to be converted into an URL
* @return a URL
* @throws MalformedURLException
* If the file protocol handler is not found (should not happen)
* or if an error occurred while constructing the URL
*/
static URL toURL(final File file) throws MalformedURLException
{
return file.toURI().toURL();
}
/**
* Private constructor so that no instances can be created.
*/
private FileLocatorUtils()
{
}
}