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