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