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