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