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