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