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