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 <b>null</b> 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}