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