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