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