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