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