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}