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    
018    package org.apache.commons.configuration;
019    
020    import java.io.File;
021    import java.io.PrintStream;
022    import java.io.PrintWriter;
023    import java.io.StringWriter;
024    import java.lang.reflect.InvocationTargetException;
025    import java.lang.reflect.Method;
026    import java.net.MalformedURLException;
027    import java.net.URL;
028    import java.util.Iterator;
029    
030    import org.apache.commons.configuration.event.ConfigurationErrorEvent;
031    import org.apache.commons.configuration.event.ConfigurationErrorListener;
032    import org.apache.commons.configuration.event.EventSource;
033    import org.apache.commons.configuration.reloading.Reloadable;
034    import org.apache.commons.configuration.tree.ExpressionEngine;
035    import org.apache.commons.lang.StringUtils;
036    import org.apache.commons.logging.Log;
037    import 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     */
048    public 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    }