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