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