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
038 * configuration files based on different criteria, e.g. URLs, files, or more
039 * complex search strategies. They also implement functionality required by the
040 * default {@link FileSystem} implementations. Most methods are intended to be
041 * used internally only by other classes in the {@code io} package.
042 * </p>
043 *
044 * @version $Id: FileLocatorUtils.java 1842194 2018-09-27 22:24:23Z ggregory $
045 * @since 2.0
046 */
047public final class FileLocatorUtils
048{
049    /**
050     * Constant for the default {@code FileSystem}. This file system is used by
051     * operations of this class if no specific file system is provided. An
052     * instance of {@link DefaultFileSystem} is used.
053     */
054    public static final FileSystem DEFAULT_FILE_SYSTEM =
055            new DefaultFileSystem();
056
057    /**
058     * Constant for the default {@code FileLocationStrategy}. This strategy is
059     * used by the {@code locate()} method if the passed in {@code FileLocator}
060     * does not define its own location strategy. The default location strategy
061     * is roughly equivalent to the search algorithm used in version 1.x of
062     * <em>Commons Configuration</em> (there it was hard-coded though). It
063     * behaves in the following way when passed a {@code FileLocator}:
064     * <ul>
065     * <li>If the {@code FileLocator} has a defined URL, this URL is used as the
066     * file's URL (without any further checks).</li>
067     * <li>Otherwise, base path and file name stored in the {@code FileLocator}
068     * are passed to the current {@code FileSystem}'s {@code locateFromURL()}
069     * method. If this results in a URL, it is returned.</li>
070     * <li>Otherwise, if the locator's file name is an absolute path to an
071     * existing file, the URL of this file is returned.</li>
072     * <li>Otherwise, the concatenation of base path and file name is
073     * constructed. If this path points to an existing file, its URL is
074     * returned.</li>
075     * <li>Otherwise, a sub directory of the current user's home directory as
076     * defined by the base path is searched for the referenced file. If the file
077     * can be found there, its URL is returned.</li>
078     * <li>Otherwise, the base path is ignored, and the file name is searched in
079     * the current user's home directory. If the file can be found there, its
080     * URL is returned.</li>
081     * <li>Otherwise, a resource with the name of the locator's file name is
082     * searched in the classpath. If it can be found, its URL is returned.</li>
083     * <li>Otherwise, the strategy gives up and returns <b>null</b> indicating
084     * that the file cannot be resolved.</li>
085     * </ul>
086     */
087    public static final FileLocationStrategy DEFAULT_LOCATION_STRATEGY =
088            initDefaultLocationStrategy();
089
090    /** Constant for the file URL protocol */
091    private static final String FILE_SCHEME = "file:";
092
093    /** The logger.*/
094    private static final Log LOG = LogFactory.getLog(FileLocatorUtils.class);
095
096    /** Property key for the base path. */
097    private static final String PROP_BASE_PATH = "basePath";
098
099    /** Property key for the encoding. */
100    private static final String PROP_ENCODING = "encoding";
101
102    /** Property key for the file name. */
103    private static final String PROP_FILE_NAME = "fileName";
104
105    /** Property key for the file system. */
106    private static final String PROP_FILE_SYSTEM = "fileSystem";
107
108    /** Property key for the location strategy. */
109    private static final String PROP_STRATEGY = "locationStrategy";
110
111    /** Property key for the source URL. */
112    private static final String PROP_SOURCE_URL = "sourceURL";
113
114    /**
115     * Private constructor so that no instances can be created.
116     */
117    private FileLocatorUtils()
118    {
119    }
120
121    /**
122     * Tries to convert the specified URL to a file object. If this fails,
123     * <b>null</b> is returned.
124     *
125     * @param url the URL
126     * @return the resulting file object
127     */
128    public static File fileFromURL(final URL url)
129    {
130        return FileUtils.toFile(url);
131    }
132
133    /**
134     * Returns an uninitialized {@code FileLocatorBuilder} which can be used
135     * for the creation of a {@code FileLocator} object. This method provides
136     * a convenient way to create file locators using a fluent API as in the
137     * following example:
138     * <pre>
139     * FileLocator locator = FileLocatorUtils.fileLocator()
140     *     .basePath(myBasePath)
141     *     .fileName("test.xml")
142     *     .create();
143     * </pre>
144     * @return a builder object for defining a {@code FileLocator}
145     */
146    public static FileLocator.FileLocatorBuilder fileLocator()
147    {
148        return fileLocator(null);
149    }
150
151    /**
152     * Returns a {@code FileLocatorBuilder} which is already initialized with
153     * the properties of the passed in {@code FileLocator}. This builder can
154     * be used to create a {@code FileLocator} object which shares properties
155     * of the original locator (e.g. the {@code FileSystem} or the encoding),
156     * but points to a different file. An example use case is as follows:
157     * <pre>
158     * FileLocator loc1 = ...
159     * FileLocator loc2 = FileLocatorUtils.fileLocator(loc1)
160     *     .setFileName("anotherTest.xml")
161     *     .create();
162     * </pre>
163     * @param src the source {@code FileLocator} (may be <b>null</b>)
164     * @return an initialized builder object for defining a {@code FileLocator}
165     */
166    public static FileLocator.FileLocatorBuilder fileLocator(final FileLocator src)
167    {
168        return new FileLocator.FileLocatorBuilder(src);
169    }
170
171    /**
172     * Creates a new {@code FileLocator} object with the properties defined in
173     * the given map. The map must be conform to the structure generated by the
174     * {@link #put(FileLocator, Map)} method; unexpected data can cause
175     * {@code ClassCastException} exceptions. The map can be <b>null</b>, then
176     * an uninitialized {@code FileLocator} is returned.
177     *
178     * @param map the map
179     * @return the new {@code FileLocator}
180     * @throws ClassCastException if the map contains invalid data
181     */
182    public static FileLocator fromMap(final Map<String, ?> map)
183    {
184        final FileLocator.FileLocatorBuilder builder = fileLocator();
185        if (map != null)
186        {
187            builder.basePath((String) map.get(PROP_BASE_PATH))
188                    .encoding((String) map.get(PROP_ENCODING))
189                    .fileName((String) map.get(PROP_FILE_NAME))
190                    .fileSystem((FileSystem) map.get(PROP_FILE_SYSTEM))
191                    .locationStrategy(
192                            (FileLocationStrategy) map.get(PROP_STRATEGY))
193                    .sourceURL((URL) map.get(PROP_SOURCE_URL));
194        }
195        return builder.create();
196    }
197
198    /**
199     * Stores the specified {@code FileLocator} in the given map. With the
200     * {@link #fromMap(Map)} method a new {@code FileLocator} with the same
201     * properties as the original one can be created.
202     *
203     * @param locator the {@code FileLocator} to be stored
204     * @param map the map in which to store the {@code FileLocator} (must not be
205     *        <b>null</b>)
206     * @throws IllegalArgumentException if the map is <b>null</b>
207     */
208    public static void put(final FileLocator locator, final Map<String, Object> map)
209    {
210        if (map == null)
211        {
212            throw new IllegalArgumentException("Map must not be null!");
213        }
214
215        if (locator != null)
216        {
217            map.put(PROP_BASE_PATH, locator.getBasePath());
218            map.put(PROP_ENCODING, locator.getEncoding());
219            map.put(PROP_FILE_NAME, locator.getFileName());
220            map.put(PROP_FILE_SYSTEM, locator.getFileSystem());
221            map.put(PROP_SOURCE_URL, locator.getSourceURL());
222            map.put(PROP_STRATEGY, locator.getLocationStrategy());
223        }
224    }
225
226    /**
227     * Checks whether the specified {@code FileLocator} contains enough
228     * information to locate a file. This is the case if a file name or a URL is
229     * defined. If the passed in {@code FileLocator} is <b>null</b>, result is
230     * <b>false</b>.
231     *
232     * @param locator the {@code FileLocator} to check
233     * @return a flag whether a file location is defined by this
234     *         {@code FileLocator}
235     */
236    public static boolean isLocationDefined(final FileLocator locator)
237    {
238        return (locator != null)
239                && (locator.getFileName() != null || locator.getSourceURL() != null);
240    }
241
242    /**
243     * Returns a flag whether all components of the given {@code FileLocator}
244     * describing the referenced file are defined. In order to reference a file,
245     * it is not necessary that all components are filled in (for instance, the
246     * URL alone is sufficient). For some use cases however, it might be of
247     * interest to have different methods for accessing the referenced file.
248     * Also, depending on the filled out properties, there is a subtle
249     * difference how the file is accessed: If only the file name is set (and
250     * optionally the base path), each time the file is accessed a
251     * {@code locate()} operation has to be performed to uniquely identify the
252     * file. If however the URL is determined once based on the other components
253     * and stored in a fully defined {@code FileLocator}, it can be used
254     * directly to identify the file. If the passed in {@code FileLocator} is
255     * <b>null</b>, result is <b>false</b>.
256     *
257     * @param locator the {@code FileLocator} to be checked (may be <b>null</b>)
258     * @return a flag whether all components describing the referenced file are
259     *         initialized
260     */
261    public static boolean isFullyInitialized(final FileLocator locator)
262    {
263        if (locator == null)
264        {
265            return false;
266        }
267        return locator.getBasePath() != null && locator.getFileName() != null
268                && locator.getSourceURL() != null;
269    }
270
271    /**
272     * Returns a {@code FileLocator} object based on the passed in one whose
273     * location is fully defined. This method ensures that all components of the
274     * {@code FileLocator} pointing to the file are set in a consistent way. In
275     * detail it behaves as follows:
276     * <ul>
277     * <li>If the {@code FileLocator} has already all components set which
278     * define the file, it is returned unchanged. <em>Note:</em> It is not
279     * checked whether all components are really consistent!</li>
280     * <li>{@link #locate(FileLocator)} is called to determine a unique URL
281     * pointing to the referenced file. If this is successful, a new
282     * {@code FileLocator} is created as a copy of the passed in one, but with
283     * all components pointing to the file derived from this URL.</li>
284     * <li>Otherwise, result is <b>null</b>.</li>
285     * </ul>
286     *
287     * @param locator the {@code FileLocator} to be completed
288     * @return a {@code FileLocator} with a fully initialized location if
289     *         possible or <b>null</b>
290     */
291    public static FileLocator fullyInitializedLocator(final FileLocator locator)
292    {
293        if (isFullyInitialized(locator))
294        {
295            // already fully initialized
296            return locator;
297        }
298
299        final URL url = locate(locator);
300        return (url != null) ? createFullyInitializedLocatorFromURL(locator,
301                url) : null;
302    }
303
304    /**
305     * Locates the provided {@code FileLocator}, returning a URL for accessing
306     * the referenced file. This method uses a {@link FileLocationStrategy} to
307     * locate the file the passed in {@code FileLocator} points to. If the
308     * {@code FileLocator} contains itself a {@code FileLocationStrategy}, it is
309     * used. Otherwise, the default {@code FileLocationStrategy} is applied. The
310     * strategy is passed the locator and a {@code FileSystem}. The resulting
311     * URL is returned. If the {@code FileLocator} is <b>null</b>, result is
312     * <b>null</b>.
313     *
314     * @param locator the {@code FileLocator} to be resolved
315     * @return the URL pointing to the referenced file or <b>null</b> if the
316     *         {@code FileLocator} could not be resolved
317     * @see #DEFAULT_LOCATION_STRATEGY
318     */
319    public static URL locate(final FileLocator locator)
320    {
321        if (locator == null)
322        {
323            return null;
324        }
325
326        return obtainLocationStrategy(locator).locate(
327                obtainFileSystem(locator), locator);
328    }
329
330    /**
331     * Tries to locate the file referenced by the passed in {@code FileLocator}.
332     * If this fails, an exception is thrown. This method works like
333     * {@link #locate(FileLocator)}; however, in case of a failed location
334     * attempt an exception is thrown.
335     *
336     * @param locator the {@code FileLocator} to be resolved
337     * @return the URL pointing to the referenced file
338     * @throws ConfigurationException if the file cannot be resolved
339     */
340    public static URL locateOrThrow(final FileLocator locator)
341            throws ConfigurationException
342    {
343        final URL url = locate(locator);
344        if (url == null)
345        {
346            throw new ConfigurationException("Could not locate: " + locator);
347        }
348        return url;
349    }
350
351    /**
352     * Return the path without the file name, for example http://xyz.net/foo/bar.xml
353     * results in http://xyz.net/foo/
354     *
355     * @param url the URL from which to extract the path
356     * @return the path component of the passed in URL
357     */
358    static String getBasePath(final URL url)
359    {
360        if (url == null)
361        {
362            return null;
363        }
364
365        String s = url.toString();
366        if (s.startsWith(FILE_SCHEME) && !s.startsWith("file://"))
367        {
368            s = "file://" + s.substring(FILE_SCHEME.length());
369        }
370
371        if (s.endsWith("/") || StringUtils.isEmpty(url.getPath()))
372        {
373            return s;
374        }
375        return s.substring(0, s.lastIndexOf("/") + 1);
376    }
377
378    /**
379     * Extract the file name from the specified URL.
380     *
381     * @param url the URL from which to extract the file name
382     * @return the extracted file name
383     */
384    static String getFileName(final URL url)
385    {
386        if (url == null)
387        {
388            return null;
389        }
390
391        final String path = url.getPath();
392
393        if (path.endsWith("/") || StringUtils.isEmpty(path))
394        {
395            return null;
396        }
397        return path.substring(path.lastIndexOf("/") + 1);
398    }
399
400    /**
401     * Tries to convert the specified base path and file name into a file object.
402     * This method is called e.g. by the save() methods of file based
403     * configurations. The parameter strings can be relative files, absolute
404     * files and URLs as well. This implementation checks first whether the passed in
405     * file name is absolute. If this is the case, it is returned. Otherwise
406     * further checks are performed whether the base path and file name can be
407     * combined to a valid URL or a valid file name. <em>Note:</em> The test
408     * if the passed in file name is absolute is performed using
409     * {@code java.io.File.isAbsolute()}. If the file name starts with a
410     * slash, this method will return <b>true</b> on Unix, but <b>false</b> on
411     * Windows. So to ensure correct behavior for relative file names on all
412     * platforms you should never let relative paths start with a slash. E.g.
413     * in a configuration definition file do not use something like that:
414     * <pre>
415     * &lt;properties fileName="/subdir/my.properties"/&gt;
416     * </pre>
417     * Under Windows this path would be resolved relative to the configuration
418     * definition file. Under Unix this would be treated as an absolute path
419     * name.
420     *
421     * @param basePath the base path
422     * @param fileName the file name (must not be <b>null</b>)
423     * @return the file object (<b>null</b> if no file can be obtained)
424     */
425    static File getFile(final String basePath, final String fileName)
426    {
427        // Check if the file name is absolute
428        final File f = new File(fileName);
429        if (f.isAbsolute())
430        {
431            return f;
432        }
433
434        // Check if URLs are involved
435        URL url;
436        try
437        {
438            url = new URL(new URL(basePath), fileName);
439        }
440        catch (final MalformedURLException mex1)
441        {
442            try
443            {
444                url = new URL(fileName);
445            }
446            catch (final MalformedURLException mex2)
447            {
448                url = null;
449            }
450        }
451
452        if (url != null)
453        {
454            return fileFromURL(url);
455        }
456
457        return constructFile(basePath, fileName);
458    }
459
460    /**
461     * Convert the specified file into an URL. This method is equivalent
462     * to file.toURI().toURL(). It was used to work around a bug in the JDK
463     * preventing the transformation of a file into an URL if the file name
464     * contains a '#' character. See the issue CONFIGURATION-300 for
465     * more details. Now that we switched to JDK 1.4 we can directly use
466     * file.toURI().toURL().
467     *
468     * @param file the file to be converted into an URL
469     */
470    static URL toURL(final File file) throws MalformedURLException
471    {
472        return file.toURI().toURL();
473    }
474
475    /**
476     * Tries to convert the specified URI to a URL. If this causes an exception,
477     * result is <b>null</b>.
478     *
479     * @param uri the URI to be converted
480     * @return the resulting URL or <b>null</b>
481     */
482    static URL convertURIToURL(final URI uri)
483    {
484        try
485        {
486            return uri.toURL();
487        }
488        catch (final MalformedURLException e)
489        {
490            return null;
491        }
492    }
493
494    /**
495     * Tries to convert the specified file to a URL. If this causes an
496     * exception, result is <b>null</b>.
497     *
498     * @param file the file to be converted
499     * @return the resulting URL or <b>null</b>
500     */
501    static URL convertFileToURL(final File file)
502    {
503        return convertURIToURL(file.toURI());
504    }
505
506    /**
507     * Tries to find a resource with the given name in the classpath.
508     *
509     * @param resourceName the name of the resource
510     * @return the URL to the found resource or <b>null</b> if the resource
511     *         cannot be found
512     */
513    static URL locateFromClasspath(final String resourceName)
514    {
515        URL url = null;
516        // attempt to load from the context classpath
517        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
518        if (loader != null)
519        {
520            url = loader.getResource(resourceName);
521
522            if (url != null)
523            {
524                LOG.debug("Loading configuration from the context classpath (" + resourceName + ")");
525            }
526        }
527
528        // attempt to load from the system classpath
529        if (url == null)
530        {
531            url = ClassLoader.getSystemResource(resourceName);
532
533            if (url != null)
534            {
535                LOG.debug("Loading configuration from the system classpath (" + resourceName + ")");
536            }
537        }
538        return url;
539    }
540
541    /**
542     * Helper method for constructing a file object from a base path and a
543     * file name. This method is called if the base path passed to
544     * {@code getURL()} does not seem to be a valid URL.
545     *
546     * @param basePath the base path
547     * @param fileName the file name (must not be <b>null</b>)
548     * @return the resulting file
549     */
550    static File constructFile(final String basePath, final String fileName)
551    {
552        File file;
553
554        final File absolute = new File(fileName);
555        if (StringUtils.isEmpty(basePath) || absolute.isAbsolute())
556        {
557            file = absolute;
558        }
559        else
560        {
561            file = new File(appendPath(basePath, fileName));
562        }
563
564        return file;
565    }
566
567    /**
568     * Extends a path by another component. The given extension is added to the
569     * already existing path adding a separator if necessary.
570     *
571     * @param path the path to be extended
572     * @param ext the extension of the path
573     * @return the extended path
574     */
575    static String appendPath(final String path, final String ext)
576    {
577        final StringBuilder fName = new StringBuilder();
578        fName.append(path);
579
580        // My best friend. Paranoia.
581        if (!path.endsWith(File.separator))
582        {
583            fName.append(File.separator);
584        }
585
586        //
587        // We have a relative path, and we have
588        // two possible forms here. If we have the
589        // "./" form then just strip that off first
590        // before continuing.
591        //
592        if (ext.startsWith("." + File.separator))
593        {
594            fName.append(ext.substring(2));
595        }
596        else
597        {
598            fName.append(ext);
599        }
600        return fName.toString();
601    }
602
603    /**
604     * Obtains a non-<b>null</b> {@code FileSystem} object from the passed in
605     * {@code FileLocator}. If the passed in {@code FileLocator} has a
606     * {@code FileSystem} object, it is returned. Otherwise, result is the
607     * default {@code FileSystem}.
608     *
609     * @param locator the {@code FileLocator} (may be <b>null</b>)
610     * @return the {@code FileSystem} to be used for this {@code FileLocator}
611     */
612    static FileSystem obtainFileSystem(final FileLocator locator)
613    {
614        return (locator != null) ? ObjectUtils.defaultIfNull(
615                locator.getFileSystem(), DEFAULT_FILE_SYSTEM)
616                : DEFAULT_FILE_SYSTEM;
617    }
618
619    /**
620     * Obtains a non <b>null</b> {@code FileLocationStrategy} object from the
621     * passed in {@code FileLocator}. If the {@code FileLocator} is not
622     * <b>null</b> and has a {@code FileLocationStrategy} defined, this strategy
623     * is returned. Otherwise, result is the default
624     * {@code FileLocationStrategy}.
625     *
626     * @param locator the {@code FileLocator}
627     * @return the {@code FileLocationStrategy} for this {@code FileLocator}
628     */
629    static FileLocationStrategy obtainLocationStrategy(final FileLocator locator)
630    {
631        return (locator != null) ? ObjectUtils.defaultIfNull(
632                locator.getLocationStrategy(), DEFAULT_LOCATION_STRATEGY)
633                : DEFAULT_LOCATION_STRATEGY;
634    }
635
636    /**
637     * Creates a fully initialized {@code FileLocator} based on the specified
638     * URL.
639     *
640     * @param src the source {@code FileLocator}
641     * @param url the URL
642     * @return the fully initialized {@code FileLocator}
643     */
644    private static FileLocator createFullyInitializedLocatorFromURL(final FileLocator src,
645            final URL url)
646    {
647        final FileLocator.FileLocatorBuilder fileLocatorBuilder = fileLocator(src);
648        if (src.getSourceURL() == null)
649        {
650            fileLocatorBuilder.sourceURL(url);
651        }
652        if (StringUtils.isBlank(src.getFileName()))
653        {
654            fileLocatorBuilder.fileName(getFileName(url));
655        }
656        if (StringUtils.isBlank(src.getBasePath()))
657        {
658            fileLocatorBuilder.basePath(getBasePath(url));
659        }
660        return fileLocatorBuilder.create();
661    }
662
663    /**
664     * Creates the default location strategy. This method creates a combined
665     * location strategy as described in the comment of the
666     * {@link #DEFAULT_LOCATION_STRATEGY} member field.
667     *
668     * @return the default {@code FileLocationStrategy}
669     */
670    private static FileLocationStrategy initDefaultLocationStrategy()
671    {
672        final FileLocationStrategy[] subStrategies =
673                new FileLocationStrategy[] {
674                        new ProvidedURLLocationStrategy(),
675                        new FileSystemLocationStrategy(),
676                        new AbsoluteNameLocationStrategy(),
677                        new BasePathLocationStrategy(),
678                        new HomeDirectoryLocationStrategy(true),
679                        new HomeDirectoryLocationStrategy(false),
680                        new ClasspathLocationStrategy()
681                };
682        return new CombinedLocationStrategy(Arrays.asList(subStrategies));
683    }
684}