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}