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}