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 */
017
018package org.apache.commons.configuration;
019
020import java.io.File;
021import java.io.PrintStream;
022import java.io.PrintWriter;
023import java.io.StringWriter;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.net.MalformedURLException;
027import java.net.URL;
028import java.util.Iterator;
029
030import org.apache.commons.configuration.event.ConfigurationErrorEvent;
031import org.apache.commons.configuration.event.ConfigurationErrorListener;
032import org.apache.commons.configuration.event.EventSource;
033import org.apache.commons.configuration.reloading.Reloadable;
034import org.apache.commons.configuration.tree.ExpressionEngine;
035import org.apache.commons.lang.StringUtils;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038
039/**
040 * Miscellaneous utility methods for configurations.
041 *
042 * @see ConfigurationConverter Utility methods to convert configurations.
043 *
044 * @author <a href="mailto:herve.quiroz@esil.univ-mrs.fr">Herve Quiroz</a>
045 * @author Emmanuel Bourg
046 * @version $Id: ConfigurationUtils.java 1208795 2011-11-30 21:18:17Z oheger $
047 */
048public final class ConfigurationUtils
049{
050    /** Constant for the file URL protocol.*/
051    static final String PROTOCOL_FILE = "file";
052
053    /** Constant for the resource path separator.*/
054    static final String RESOURCE_PATH_SEPARATOR = "/";
055
056    /** Constant for the file URL protocol */
057    private static final String FILE_SCHEME = "file:";
058
059    /** Constant for the name of the clone() method.*/
060    private static final String METHOD_CLONE = "clone";
061
062    /** Constant for parsing numbers in hex format. */
063    private static final int HEX = 16;
064
065    /** The logger.*/
066    private static final Log LOG = LogFactory.getLog(ConfigurationUtils.class);
067
068    /**
069     * Private constructor. Prevents instances from being created.
070     */
071    private ConfigurationUtils()
072    {
073        // to prevent instantiation...
074    }
075
076    /**
077     * Dump the configuration key/value mappings to some ouput stream.
078     *
079     * @param configuration the configuration
080     * @param out the output stream to dump the configuration to
081     */
082    public static void dump(Configuration configuration, PrintStream out)
083    {
084        dump(configuration, new PrintWriter(out));
085    }
086
087    /**
088     * Dump the configuration key/value mappings to some writer.
089     *
090     * @param configuration the configuration
091     * @param out the writer to dump the configuration to
092     */
093    public static void dump(Configuration configuration, PrintWriter out)
094    {
095        for (Iterator<String> keys = configuration.getKeys(); keys.hasNext();)
096        {
097            String key = keys.next();
098            Object value = configuration.getProperty(key);
099            out.print(key);
100            out.print("=");
101            out.print(value);
102
103            if (keys.hasNext())
104            {
105                out.println();
106            }
107        }
108
109        out.flush();
110    }
111
112    /**
113     * Get a string representation of the key/value mappings of a
114     * configuration.
115     *
116     * @param configuration the configuration
117     * @return a string representation of the configuration
118     */
119    public static String toString(Configuration configuration)
120    {
121        StringWriter writer = new StringWriter();
122        dump(configuration, new PrintWriter(writer));
123        return writer.toString();
124    }
125
126    /**
127     * <p>Copy all properties from the source configuration to the target
128     * configuration. Properties in the target configuration are replaced with
129     * the properties with the same key in the source configuration.</p>
130     * <p><em>Note:</em> This method is not able to handle some specifics of
131     * configurations derived from {@code AbstractConfiguration} (e.g.
132     * list delimiters). For a full support of all of these features the
133     * {@code copy()} method of {@code AbstractConfiguration} should
134     * be used. In a future release this method might become deprecated.</p>
135     *
136     * @param source the source configuration
137     * @param target the target configuration
138     * @since 1.1
139     */
140    public static void copy(Configuration source, Configuration target)
141    {
142        for (Iterator<String> keys = source.getKeys(); keys.hasNext();)
143        {
144            String key = keys.next();
145            target.setProperty(key, source.getProperty(key));
146        }
147    }
148
149    /**
150     * <p>Append all properties from the source configuration to the target
151     * configuration. Properties in the source configuration are appended to
152     * the properties with the same key in the target configuration.</p>
153     * <p><em>Note:</em> This method is not able to handle some specifics of
154     * configurations derived from {@code AbstractConfiguration} (e.g.
155     * list delimiters). For a full support of all of these features the
156     * {@code copy()} method of {@code AbstractConfiguration} should
157     * be used. In a future release this method might become deprecated.</p>
158     *
159     * @param source the source configuration
160     * @param target the target configuration
161     * @since 1.1
162     */
163    public static void append(Configuration source, Configuration target)
164    {
165        for (Iterator<String> keys = source.getKeys(); keys.hasNext();)
166        {
167            String key = keys.next();
168            target.addProperty(key, source.getProperty(key));
169        }
170    }
171
172    /**
173     * Converts the passed in configuration to a hierarchical one. If the
174     * configuration is already hierarchical, it is directly returned. Otherwise
175     * all properties are copied into a new hierarchical configuration.
176     *
177     * @param conf the configuration to convert
178     * @return the new hierarchical configuration (the result is <b>null</b> if
179     * and only if the passed in configuration is <b>null</b>)
180     * @since 1.3
181     */
182    public static HierarchicalConfiguration convertToHierarchical(
183            Configuration conf)
184    {
185        return convertToHierarchical(conf, null);
186    }
187
188    /**
189     * Converts the passed in {@code Configuration} object to a
190     * hierarchical one using the specified {@code ExpressionEngine}. This
191     * conversion works by adding the keys found in the configuration to a newly
192     * created hierarchical configuration. When adding new keys to a
193     * hierarchical configuration the keys are interpreted by its
194     * {@code ExpressionEngine}. If they contain special characters (e.g.
195     * brackets) that are treated in a special way by the default expression
196     * engine, it may be necessary using a specific engine that can deal with
197     * such characters. Otherwise <b>null</b> can be passed in for the
198     * {@code ExpressionEngine}; then the default expression engine is
199     * used. If the passed in configuration is already hierarchical, it is
200     * directly returned. (However, the {@code ExpressionEngine} is set if
201     * it is not <b>null</b>.) Otherwise all properties are copied into a new
202     * hierarchical configuration.
203     *
204     * @param conf the configuration to convert
205     * @param engine the {@code ExpressionEngine} for the hierarchical
206     *        configuration or <b>null</b> for the default
207     * @return the new hierarchical configuration (the result is <b>null</b> if
208     *         and only if the passed in configuration is <b>null</b>)
209     * @since 1.6
210     */
211    public static HierarchicalConfiguration convertToHierarchical(
212            Configuration conf, ExpressionEngine engine)
213    {
214        if (conf == null)
215        {
216            return null;
217        }
218
219        if (conf instanceof HierarchicalConfiguration)
220        {
221            HierarchicalConfiguration hc;
222            if (conf instanceof Reloadable)
223            {
224                Object lock = ((Reloadable) conf).getReloadLock();
225                synchronized (lock)
226                {
227                    hc = new HierarchicalConfiguration((HierarchicalConfiguration) conf);
228                }
229            }
230            else
231            {
232                hc = (HierarchicalConfiguration) conf;
233            }
234            if (engine != null)
235            {
236                hc.setExpressionEngine(engine);
237            }
238
239            return hc;
240        }
241        else
242        {
243            HierarchicalConfiguration hc = new HierarchicalConfiguration();
244            if (engine != null)
245            {
246                hc.setExpressionEngine(engine);
247            }
248
249            // Workaround for problem with copy()
250            boolean delimiterParsingStatus = hc.isDelimiterParsingDisabled();
251            hc.setDelimiterParsingDisabled(true);
252            hc.append(conf);
253            hc.setDelimiterParsingDisabled(delimiterParsingStatus);
254            return hc;
255        }
256    }
257
258    /**
259     * Clones the given configuration object if this is possible. If the passed
260     * in configuration object implements the {@code Cloneable}
261     * interface, its {@code clone()} method will be invoked. Otherwise
262     * an exception will be thrown.
263     *
264     * @param config the configuration object to be cloned (can be <b>null</b>)
265     * @return the cloned configuration (<b>null</b> if the argument was
266     * <b>null</b>, too)
267     * @throws ConfigurationRuntimeException if cloning is not supported for
268     * this object
269     * @since 1.3
270     */
271    public static Configuration cloneConfiguration(Configuration config)
272            throws ConfigurationRuntimeException
273    {
274        if (config == null)
275        {
276            return null;
277        }
278        else
279        {
280            try
281            {
282                return (Configuration) clone(config);
283            }
284            catch (CloneNotSupportedException cnex)
285            {
286                throw new ConfigurationRuntimeException(cnex);
287            }
288        }
289    }
290
291    /**
292     * An internally used helper method for cloning objects. This implementation
293     * is not very sophisticated nor efficient. Maybe it can be replaced by an
294     * implementation from Commons Lang later. The method checks whether the
295     * passed in object implements the {@code Cloneable} interface. If
296     * this is the case, the {@code clone()} method is invoked by
297     * reflection. Errors that occur during the cloning process are re-thrown as
298     * runtime exceptions.
299     *
300     * @param obj the object to be cloned
301     * @return the cloned object
302     * @throws CloneNotSupportedException if the object cannot be cloned
303     */
304    static Object clone(Object obj) throws CloneNotSupportedException
305    {
306        if (obj instanceof Cloneable)
307        {
308            try
309            {
310                Method m = obj.getClass().getMethod(METHOD_CLONE);
311                return m.invoke(obj);
312            }
313            catch (NoSuchMethodException nmex)
314            {
315                throw new CloneNotSupportedException(
316                        "No clone() method found for class"
317                                + obj.getClass().getName());
318            }
319            catch (IllegalAccessException iaex)
320            {
321                throw new ConfigurationRuntimeException(iaex);
322            }
323            catch (InvocationTargetException itex)
324            {
325                throw new ConfigurationRuntimeException(itex);
326            }
327        }
328        else
329        {
330            throw new CloneNotSupportedException(obj.getClass().getName()
331                    + " does not implement Cloneable");
332        }
333    }
334
335    /**
336     * Constructs a URL from a base path and a file name. The file name can
337     * be absolute, relative or a full URL. If necessary the base path URL is
338     * applied.
339     *
340     * @param basePath the base path URL (can be <b>null</b>)
341     * @param file the file name
342     * @return the resulting URL
343     * @throws MalformedURLException if URLs are invalid
344     */
345    public static URL getURL(String basePath, String file) throws MalformedURLException
346    {
347        return FileSystem.getDefaultFileSystem().getURL(basePath, file);
348    }
349
350    /**
351     * Helper method for constructing a file object from a base path and a
352     * file name. This method is called if the base path passed to
353     * {@code getURL()} does not seem to be a valid URL.
354     *
355     * @param basePath the base path
356     * @param fileName the file name
357     * @return the resulting file
358     */
359    static File constructFile(String basePath, String fileName)
360    {
361        File file;
362
363        File absolute = null;
364        if (fileName != null)
365        {
366            absolute = new File(fileName);
367        }
368
369        if (StringUtils.isEmpty(basePath) || (absolute != null && absolute.isAbsolute()))
370        {
371            file = new File(fileName);
372        }
373        else
374        {
375            StringBuilder fName = new StringBuilder();
376            fName.append(basePath);
377
378            // My best friend. Paranoia.
379            if (!basePath.endsWith(File.separator))
380            {
381                fName.append(File.separator);
382            }
383
384            //
385            // We have a relative path, and we have
386            // two possible forms here. If we have the
387            // "./" form then just strip that off first
388            // before continuing.
389            //
390            if (fileName.startsWith("." + File.separator))
391            {
392                fName.append(fileName.substring(2));
393            }
394            else
395            {
396                fName.append(fileName);
397            }
398
399            file = new File(fName.toString());
400        }
401
402        return file;
403    }
404
405    /**
406     * Return the location of the specified resource by searching the user home
407     * directory, the current classpath and the system classpath.
408     *
409     * @param name the name of the resource
410     *
411     * @return the location of the resource
412     */
413    public static URL locate(String name)
414    {
415        return locate(null, name);
416    }
417
418    /**
419     * Return the location of the specified resource by searching the user home
420     * directory, the current classpath and the system classpath.
421     *
422     * @param base the base path of the resource
423     * @param name the name of the resource
424     *
425     * @return the location of the resource
426     */
427    public static URL locate(String base, String name)
428    {
429        return locate(FileSystem.getDefaultFileSystem(), base, name);
430    }
431
432    /**
433     * Return the location of the specified resource by searching the user home
434     * directory, the current classpath and the system classpath.
435     *
436     * @param fileSystem the FileSystem to use.
437     * @param base the base path of the resource
438     * @param name the name of the resource
439     *
440     * @return the location of the resource
441     */
442    public static URL locate(FileSystem fileSystem, String base, String name)
443    {
444        if (LOG.isDebugEnabled())
445        {
446            StringBuilder buf = new StringBuilder();
447            buf.append("ConfigurationUtils.locate(): base is ").append(base);
448            buf.append(", name is ").append(name);
449            LOG.debug(buf.toString());
450        }
451
452        if (name == null)
453        {
454            // undefined, always return null
455            return null;
456        }
457
458        // attempt to create an URL directly
459
460        URL url = fileSystem.locateFromURL(base, name);
461
462        // attempt to load from an absolute path
463        if (url == null)
464        {
465            File file = new File(name);
466            if (file.isAbsolute() && file.exists()) // already absolute?
467            {
468                try
469                {
470                    url = toURL(file);
471                    LOG.debug("Loading configuration from the absolute path " + name);
472                }
473                catch (MalformedURLException e)
474                {
475                    LOG.warn("Could not obtain URL from file", e);
476                }
477            }
478        }
479
480        // attempt to load from the base directory
481        if (url == null)
482        {
483            try
484            {
485                File file = constructFile(base, name);
486                if (file != null && file.exists())
487                {
488                    url = toURL(file);
489                }
490
491                if (url != null)
492                {
493                    LOG.debug("Loading configuration from the path " + file);
494                }
495            }
496            catch (MalformedURLException e)
497            {
498                LOG.warn("Could not obtain URL from file", e);
499            }
500        }
501
502        // attempt to load from the user home directory
503        if (url == null)
504        {
505            try
506            {
507                File file = constructFile(System.getProperty("user.home"), name);
508                if (file != null && file.exists())
509                {
510                    url = toURL(file);
511                }
512
513                if (url != null)
514                {
515                    LOG.debug("Loading configuration from the home path " + file);
516                }
517
518            }
519            catch (MalformedURLException e)
520            {
521                LOG.warn("Could not obtain URL from file", e);
522            }
523        }
524
525        // attempt to load from classpath
526        if (url == null)
527        {
528            url = locateFromClasspath(name);
529        }
530        return url;
531    }
532
533    /**
534     * Tries to find a resource with the given name in the classpath.
535     * @param resourceName the name of the resource
536     * @return the URL to the found resource or <b>null</b> if the resource
537     * cannot be found
538     */
539    static URL locateFromClasspath(String resourceName)
540    {
541        URL url = null;
542        // attempt to load from the context classpath
543        ClassLoader loader = Thread.currentThread().getContextClassLoader();
544        if (loader != null)
545        {
546            url = loader.getResource(resourceName);
547
548            if (url != null)
549            {
550                LOG.debug("Loading configuration from the context classpath (" + resourceName + ")");
551            }
552        }
553
554        // attempt to load from the system classpath
555        if (url == null)
556        {
557            url = ClassLoader.getSystemResource(resourceName);
558
559            if (url != null)
560            {
561                LOG.debug("Loading configuration from the system classpath (" + resourceName + ")");
562            }
563        }
564        return url;
565    }
566
567    /**
568     * Return the path without the file name, for example http://xyz.net/foo/bar.xml
569     * results in http://xyz.net/foo/
570     *
571     * @param url the URL from which to extract the path
572     * @return the path component of the passed in URL
573     */
574    static String getBasePath(URL url)
575    {
576        if (url == null)
577        {
578            return null;
579        }
580
581        String s = url.toString();
582        if (s.startsWith(FILE_SCHEME) && !s.startsWith("file://"))
583        {
584            s = "file://" + s.substring(FILE_SCHEME.length());
585        }
586
587        if (s.endsWith("/") || StringUtils.isEmpty(url.getPath()))
588        {
589            return s;
590        }
591        else
592        {
593            return s.substring(0, s.lastIndexOf("/") + 1);
594        }
595    }
596
597    /**
598     * Extract the file name from the specified URL.
599     *
600     * @param url the URL from which to extract the file name
601     * @return the extracted file name
602     */
603    static String getFileName(URL url)
604    {
605        if (url == null)
606        {
607            return null;
608        }
609
610        String path = url.getPath();
611
612        if (path.endsWith("/") || StringUtils.isEmpty(path))
613        {
614            return null;
615        }
616        else
617        {
618            return path.substring(path.lastIndexOf("/") + 1);
619        }
620    }
621
622    /**
623     * Tries to convert the specified base path and file name into a file object.
624     * This method is called e.g. by the save() methods of file based
625     * configurations. The parameter strings can be relative files, absolute
626     * files and URLs as well. This implementation checks first whether the passed in
627     * file name is absolute. If this is the case, it is returned. Otherwise
628     * further checks are performed whether the base path and file name can be
629     * combined to a valid URL or a valid file name. <em>Note:</em> The test
630     * if the passed in file name is absolute is performed using
631     * {@code java.io.File.isAbsolute()}. If the file name starts with a
632     * slash, this method will return <b>true</b> on Unix, but <b>false</b> on
633     * Windows. So to ensure correct behavior for relative file names on all
634     * platforms you should never let relative paths start with a slash. E.g.
635     * in a configuration definition file do not use something like that:
636     * <pre>
637     * &lt;properties fileName="/subdir/my.properties"/&gt;
638     * </pre>
639     * Under Windows this path would be resolved relative to the configuration
640     * definition file. Under Unix this would be treated as an absolute path
641     * name.
642     *
643     * @param basePath the base path
644     * @param fileName the file name
645     * @return the file object (<b>null</b> if no file can be obtained)
646     */
647    public static File getFile(String basePath, String fileName)
648    {
649        // Check if the file name is absolute
650        File f = new File(fileName);
651        if (f.isAbsolute())
652        {
653            return f;
654        }
655
656        // Check if URLs are involved
657        URL url;
658        try
659        {
660            url = new URL(new URL(basePath), fileName);
661        }
662        catch (MalformedURLException mex1)
663        {
664            try
665            {
666                url = new URL(fileName);
667            }
668            catch (MalformedURLException mex2)
669            {
670                url = null;
671            }
672        }
673
674        if (url != null)
675        {
676            return fileFromURL(url);
677        }
678
679        return constructFile(basePath, fileName);
680    }
681
682    /**
683     * Tries to convert the specified URL to a file object. If this fails,
684     * <b>null</b> is returned. Note: This code has been copied from the
685     * {@code FileUtils} class from <em>Commons IO</em>.
686     *
687     * @param url the URL
688     * @return the resulting file object
689     */
690    public static File fileFromURL(URL url)
691    {
692        if (url == null || !url.getProtocol().equals(PROTOCOL_FILE))
693        {
694            return null;
695        }
696        else
697        {
698            String filename = url.getFile().replace('/', File.separatorChar);
699            int pos = 0;
700            while ((pos = filename.indexOf('%', pos)) >= 0)
701            {
702                if (pos + 2 < filename.length())
703                {
704                    String hexStr = filename.substring(pos + 1, pos + 3);
705                    char ch = (char) Integer.parseInt(hexStr, HEX);
706                    filename = filename.substring(0, pos) + ch
707                            + filename.substring(pos + 3);
708                }
709            }
710            return new File(filename);
711        }
712    }
713
714    /**
715     * Convert the specified file into an URL. This method is equivalent
716     * to file.toURI().toURL(). It was used to work around a bug in the JDK
717     * preventing the transformation of a file into an URL if the file name
718     * contains a '#' character. See the issue CONFIGURATION-300 for
719     * more details. Now that we switched to JDK 1.4 we can directly use
720     * file.toURI().toURL().
721     *
722     * @param file the file to be converted into an URL
723     */
724    static URL toURL(File file) throws MalformedURLException
725    {
726        return file.toURI().toURL();
727    }
728
729    /**
730     * Enables runtime exceptions for the specified configuration object. This
731     * method can be used for configuration implementations that may face errors
732     * on normal property access, e.g. {@code DatabaseConfiguration} or
733     * {@code JNDIConfiguration}. Per default such errors are simply
734     * logged and then ignored. This implementation will register a special
735     * {@link ConfigurationErrorListener} that throws a runtime
736     * exception (namely a {@code ConfigurationRuntimeException}) on
737     * each received error event.
738     *
739     * @param src the configuration, for which runtime exceptions are to be
740     * enabled; this configuration must be derived from
741     * {@link EventSource}
742     */
743    public static void enableRuntimeExceptions(Configuration src)
744    {
745        if (!(src instanceof EventSource))
746        {
747            throw new IllegalArgumentException(
748                    "Configuration must be derived from EventSource!");
749        }
750        ((EventSource) src).addErrorListener(new ConfigurationErrorListener()
751        {
752            public void configurationError(ConfigurationErrorEvent event)
753            {
754                // Throw a runtime exception
755                throw new ConfigurationRuntimeException(event.getCause());
756            }
757        });
758    }
759}