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