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