    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    }