    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    package org.apache.commons.configuration.resolver;
    018
    019    import org.xml.sax.EntityResolver;
    020    import org.xml.sax.InputSource;
    021    import org.xml.sax.SAXException;
    022    import org.apache.xml.resolver.CatalogException;
    023    import org.apache.xml.resolver.readers.CatalogReader;
    024    import org.apache.commons.logging.Log;
    025    import org.apache.commons.logging.LogFactory;
    026    import org.apache.commons.configuration.FileSystem;
    027    import org.apache.commons.configuration.ConfigurationException;
    028    import org.apache.commons.configuration.ConfigurationUtils;
    029    import org.apache.commons.lang.text.StrSubstitutor;
    030
    031    import java.io.InputStream;
    032    import java.io.IOException;
    033    import java.net.FileNameMap;
    034    import java.net.URLConnection;
    035    import java.net.URL;
    036    import java.util.Vector;
    037
    038    /**
    039     * Thin wrapper around xml commons CatalogResolver to allow list of catalogs
    040     * to be provided.
    041     * @author <a
    042     * href="http://commons.apache.org/configuration/team-list.html">Commons
    043     * Configuration team</a>
    044     * @since 1.7
    045     * @version $Id: CatalogResolver.java 952631 2010-06-08 12:09:29Z sebb $
    046     */
    047    public class CatalogResolver implements EntityResolver
    048    {
    049        /**
    050         * Debug everything.
    051         */
    052        private static final int DEBUG_ALL = 9;
    053
    054        /**
    055         * Normal debug setting.
    056         */
    057        private static final int DEBUG_NORMAL = 4;
    058
    059        /**
    060         * Debug nothing.
    061         */
    062        private static final int DEBUG_NONE = 0;
    063
    064        /**
    065         * The CatalogManager
    066         */
    067        protected CatalogManager manager = new CatalogManager();
    068
    069        /**
    070         * The FileSystem in use.
    071         */
    072        protected FileSystem fs = FileSystem.getDefaultFileSystem();
    073
    074        /**
    075         * The CatalogResolver
    076         */
    077        private org.apache.xml.resolver.tools.CatalogResolver resolver;
    078
    079        /**
    080         * Stores the logger.
    081         */
    082        private Log log;
    083
    084        /**
    085         * Constructs the CatalogResolver
    086         */
    087        public CatalogResolver()
    088        {
    089            manager.setIgnoreMissingProperties(true);
    090            manager.setUseStaticCatalog(false);
    091            manager.setFileSystem(fs);
    092            setLogger(null);
    093        }
    094
    095        /**
    096         * Set the list of catalog file names
    097         *
    098         * @param catalogs The delimited list of catalog files.
    099         */
    100        public void setCatalogFiles(String catalogs)
    101        {
    102            manager.setCatalogFiles(catalogs);
    103        }
    104
    105        /**
    106         * Set the FileSystem.
    107         * @param fileSystem The FileSystem.
    108         */
    109        public void setFileSystem(FileSystem fileSystem)
    110        {
    111            this.fs = fileSystem;
    112            manager.setFileSystem(fileSystem);
    113        }
    114
    115        /**
    116         * Set the base path.
    117         * @param baseDir The base path String.
    118         */
    119        public void setBaseDir(String baseDir)
    120        {
    121            manager.setBaseDir(baseDir);
    122        }
    123
    124        /**
    125         * Set the StrSubstitutor.
    126         * @param substitutor The StrSubstitutor.
    127         */
    128        public void setSubstitutor(StrSubstitutor substitutor)
    129        {
    130            manager.setSubstitutor(substitutor);
    131        }
    132
    133        /**
    134         * Enables debug logging of xml-commons Catalog processing.
    135         * @param debug True if debugging should be enabled, false otherwise.
    136         */
    137        public void setDebug(boolean debug)
    138        {
    139            if (debug)
    140            {
    141                manager.setVerbosity(DEBUG_ALL);
    142            }
    143            else
    144            {
    145                manager.setVerbosity(DEBUG_NONE);
    146            }
    147        }
    148
    149        /**
    150         * Implements the <code>resolveEntity</code> method
    151         * for the SAX interface.
    152         * <p/>
    153         * <p>Presented with an optional public identifier and a system
    154         * identifier, this function attempts to locate a mapping in the
    155         * catalogs.</p>
    156         * <p/>
    157         * <p>If such a mapping is found, the resolver attempts to open
    158         * the mapped value as an InputSource and return it. Exceptions are
    159         * ignored and null is returned if the mapped value cannot be opened
    160         * as an input source.</p>
    161         * <p/>
    162         * <p>If no mapping is found (or an error occurs attempting to open
    163         * the mapped value as an input source), null is returned and the system
    164         * will use the specified system identifier as if no entityResolver
    165         * was specified.</p>
    166         *
    167         * @param publicId The public identifier for the entity in question.
    168         *                 This may be null.
    169         * @param systemId The system identifier for the entity in question.
    170         *                 XML requires a system identifier on all external entities, so this
    171         *                 value is always specified.
    172         * @return An InputSource for the mapped identifier, or null.
    173         * @throws SAXException if an error occurs.
    174         */
    175        public InputSource resolveEntity(String publicId, String systemId)
    176                throws SAXException
    177        {
    178            String resolved = getResolver().getResolvedEntity(publicId, systemId);
    179
    180            if (resolved != null)
    181            {
    182                String badFilePrefix = "file://";
    183                String correctFilePrefix = "file:///";
    184
    185                // Java 5 has a bug when constructing file URLS
    186                if (resolved.startsWith(badFilePrefix) && !resolved.startsWith(correctFilePrefix))
    187                {
    188                    resolved = correctFilePrefix + resolved.substring(badFilePrefix.length());
    189                }
    190
    191                try
    192                {
    193                    InputStream is = fs.getInputStream(null, resolved);
    194                    InputSource iSource = new InputSource(resolved);
    195                    iSource.setPublicId(publicId);
    196                    iSource.setByteStream(is);
    197                    return iSource;
    198                }
    199                catch (Exception e)
    200                {
    201                    log.warn("Failed to create InputSource for " + resolved + " ("
    202                                    + e.toString() + ")");
    203                    return null;
    204                }
    205            }
    206
    207            return null;
    208        }
    209
    210        /**
    211         * Returns the logger used by this configuration object.
    212         *
    213         * @return the logger
    214         */
    215        public Log getLogger()
    216        {
    217            return log;
    218        }
    219
    220        /**
    221         * Allows to set the logger to be used by this configuration object. This
    222         * method makes it possible for clients to exactly control logging behavior.
    223         * Per default a logger is set that will ignore all log messages. Derived
    224         * classes that want to enable logging should call this method during their
    225         * initialization with the logger to be used.
    226         *
    227         * @param log the new logger
    228         */
    229        public void setLogger(Log log)
    230        {
    231            this.log = (log != null) ? log : LogFactory.getLog(CatalogResolver.class);
    232        }
    233
    234        private synchronized org.apache.xml.resolver.tools.CatalogResolver getResolver()
    235        {
    236            if (resolver == null)
    237            {
    238                resolver = new org.apache.xml.resolver.tools.CatalogResolver(manager);
    239            }
    240            return resolver;
    241        }
    242
    243        /**
    244         * Extend the CatalogManager to make the FileSystem and base directory accessible.
    245         */
    246        public static class CatalogManager extends org.apache.xml.resolver.CatalogManager
    247        {
    248            /** The static catalog used by this manager. */
    249            private static org.apache.xml.resolver.Catalog staticCatalog;
    250
    251            /** The FileSystem */
    252            private FileSystem fs;
    253
    254            /** The base directory */
    255            private String baseDir = System.getProperty("user.dir");
    256
    257            /** The String Substitutor */
    258            private StrSubstitutor substitutor;
    259
    260            /**
    261             * Set the FileSystem
    262             * @param fileSystem The FileSystem in use.
    263             */
    264            public void setFileSystem(FileSystem fileSystem)
    265            {
    266                this.fs = fileSystem;
    267            }
    268
    269            /**
    270             * Retrieve the FileSystem.
    271             * @return The FileSystem.
    272             */
    273            public FileSystem getFileSystem()
    274            {
    275                return this.fs;
    276            }
    277
    278            /**
    279             * Set the base directory.
    280             * @param baseDir The base directory.
    281             */
    282            public void setBaseDir(String baseDir)
    283            {
    284                if (baseDir != null)
    285                {
    286                    this.baseDir = baseDir;
    287                }
    288            }
    289
    290            /**
    291             * Return the base directory.
    292             * @return The base directory.
    293             */
    294            public String getBaseDir()
    295            {
    296                return this.baseDir;
    297            }
    298
    299            public void setSubstitutor(StrSubstitutor substitutor)
    300            {
    301                this.substitutor = substitutor;
    302            }
    303
    304            public StrSubstitutor getStrSubstitutor()
    305            {
    306                return this.substitutor;
    307            }
    308
    309
    310            /**
    311             * Get a new catalog instance. This method is only overridden because xml-resolver
    312             * might be in a parent ClassLoader and will be incapable of loading our Catalog
    313             * implementation.
    314             *
    315             * This method always returns a new instance of the underlying catalog class.
    316             * @return the Catalog.
    317             */
    318            public org.apache.xml.resolver.Catalog getPrivateCatalog()
    319            {
    320                org.apache.xml.resolver.Catalog catalog = staticCatalog;
    321
    322                if (catalog == null || !getUseStaticCatalog())
    323                {
    324                    try
    325                    {
    326                        catalog = new Catalog();
    327                        catalog.setCatalogManager(this);
    328                        catalog.setupReaders();
    329                        catalog.loadSystemCatalogs();
    330                    }
    331                    catch (Exception ex)
    332                    {
    333                        ex.printStackTrace();
    334                    }
    335
    336                    if (getUseStaticCatalog())
    337                    {
    338                        staticCatalog = catalog;
    339                    }
    340                }
    341
    342                return catalog;
    343            }
    344
    345            /**
    346             * Get a catalog instance.
    347             *
    348             * If this manager uses static catalogs, the same static catalog will
    349             * always be returned. Otherwise a new catalog will be returned.
    350             * @return The Catalog.
    351             */
    352            public org.apache.xml.resolver.Catalog getCatalog()
    353            {
    354                return getPrivateCatalog();
    355            }
    356        }
    357
    358        /**
    359         * Overrides the Catalog implementation to use the underlying FileSystem.
    360         */
    361        public static class Catalog extends org.apache.xml.resolver.Catalog
    362        {
    363            /** The FileSystem */
    364            private FileSystem fs;
    365
    366            /** FileNameMap to determine the mime type */
    367            private FileNameMap fileNameMap = URLConnection.getFileNameMap();
    368
    369            /**
    370             * Load the catalogs.
    371             * @throws IOException if an error occurs.
    372             */
    373            public void loadSystemCatalogs() throws IOException
    374            {
    375                fs = ((CatalogManager) catalogManager).getFileSystem();
    376                String base = ((CatalogManager) catalogManager).getBaseDir();
    377
    378                Vector catalogs = catalogManager.getCatalogFiles();
    379                if (catalogs != null)
    380                {
    381                    for (int count = 0; count < catalogs.size(); count++)
    382                    {
    383                        String fileName = (String) catalogs.elementAt(count);
    384
    385                        URL url = null;
    386                        InputStream is = null;
    387
    388                        try
    389                        {
    390                            url = ConfigurationUtils.locate(fs, base, fileName);
    391                            if (url != null)
    392                            {
    393                                is = fs.getInputStream(url);
    394                            }
    395                        }
    396                        catch (ConfigurationException ce)
    397                        {
    398                            String name = (url == null) ? fileName : url.toString();
    399                            // Ignore the exception.
    400                            catalogManager.debug.message(DEBUG_ALL,
    401                                "Unable to get input stream for " + name + ". " + ce.getMessage());
    402                        }
    403                        if (is != null)
    404                        {
    405                            String mimeType = fileNameMap.getContentTypeFor(fileName);
    406                            try
    407                            {
    408                                if (mimeType != null)
    409                                {
    410                                    parseCatalog(mimeType, is);
    411                                    continue;
    412                                }
    413                            }
    414                            catch (Exception ex)
    415                            {
    416                                // Ignore the exception.
    417                                catalogManager.debug.message(DEBUG_ALL,
    418                                    "Exception caught parsing input stream for " + fileName + ". "
    419                                    + ex.getMessage());
    420                            }
    421                            finally
    422                            {
    423                                is.close();
    424                            }
    425                        }
    426                        parseCatalog(base, fileName);
    427                    }
    428                }
    429
    430            }
    431
    432            /**
    433             * Parse the specified catalog file.
    434             * @param baseDir The base directory, if not included in the file name.
    435             * @param fileName The catalog file. May be a full URI String.
    436             * @throws IOException If an error occurs.
    437             */
    438            public void parseCatalog(String baseDir, String fileName) throws IOException
    439            {
    440                base = ConfigurationUtils.locate(fs, baseDir, fileName);
    441                catalogCwd = base;
    442                default_override = catalogManager.getPreferPublic();
    443                catalogManager.debug.message(DEBUG_NORMAL, "Parse catalog: " + fileName);
    444
    445                boolean parsed = false;
    446
    447                for (int count = 0; !parsed && count < readerArr.size(); count++)
    448                {
    449                    CatalogReader reader = (CatalogReader) readerArr.get(count);
    450                    InputStream inStream;
    451
    452                    try
    453                    {
    454                        inStream = fs.getInputStream(base);
    455                    }
    456                    catch (Exception ex)
    457                    {
    458                        catalogManager.debug.message(DEBUG_NORMAL, "Unable to access " + base
    459                            + ex.getMessage());
    460                        break;
    461                    }
    462
    463                    try
    464                    {
    465                        reader.readCatalog(this, inStream);
    466                        parsed = true;
    467                    }
    468                    catch (CatalogException ce)
    469                    {
    470                        catalogManager.debug.message(DEBUG_NORMAL, "Parse failed for " + fileName
    471                                + ce.getMessage());
    472                        if (ce.getExceptionType() == CatalogException.PARSE_FAILED)
    473                        {
    474                            break;
    475                        }
    476                        else
    477                        {
    478                            // try again!
    479                            continue;
    480                        }
    481                    }
    482                    finally
    483                    {
    484                        try
    485                        {
    486                            inStream.close();
    487                        }
    488                        catch (IOException ioe)
    489                        {
    490                            // Ignore the exception.
    491                            inStream = null;
    492                        }
    493                    }
    494                }
    495
    496                if (parsed)
    497                {
    498                    parsePendingCatalogs();
    499                }
    500            }
    501
    502            /**
    503             * Perform character normalization on a URI reference.
    504             *
    505             * @param uriref The URI reference
    506             * @return The normalized URI reference.
    507             */
    508            protected String normalizeURI(String uriref)
    509            {
    510                StrSubstitutor substitutor = ((CatalogManager) catalogManager).getStrSubstitutor();
    511                String resolved = substitutor != null ? substitutor.replace(uriref) : uriref;
    512                return super.normalizeURI(resolved);
    513            }
    514        }
    515    }