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.configuration.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.configuration.ConfigurationException;
027import org.apache.commons.configuration.ConfigurationUtils;
028import org.apache.commons.configuration.FileSystem;
029import org.apache.commons.lang.text.StrSubstitutor;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
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 1301991 2012-03-17 20:18:02Z sebb $
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    protected CatalogManager manager = new CatalogManager();
068
069    /**
070     * The FileSystem in use.
071     */
072    protected FileSystem fs = FileSystem.getDefaultFileSystem();
073
074    /**
075     * The CatalogResolver
076     */
077    private org.apache.xml.resolver.tools.CatalogResolver resolver;
078
079    /**
080     * Stores the logger.
081     */
082    private Log 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        setLogger(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(String catalogs)
101    {
102        manager.setCatalogFiles(catalogs);
103    }
104
105    /**
106     * Set the FileSystem.
107     * @param fileSystem The FileSystem.
108     */
109    public void setFileSystem(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(String baseDir)
120    {
121        manager.setBaseDir(baseDir);
122    }
123
124    /**
125     * Set the StrSubstitutor.
126     * @param substitutor The StrSubstitutor.
127     */
128    public void setSubstitutor(StrSubstitutor substitutor)
129    {
130        manager.setSubstitutor(substitutor);
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(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     * 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/>
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/>
162     * <p>If no mapping is found (or an error occurs attempting to open
163     * the mapped value as an input source), null is returned and the system
164     * will use the specified system identifier as if no entityResolver
165     * was specified.</p>
166     *
167     * @param publicId The public identifier for the entity in question.
168     *                 This may be null.
169     * @param systemId The system identifier for the entity in question.
170     *                 XML requires a system identifier on all external entities, so this
171     *                 value is always specified.
172     * @return An InputSource for the mapped identifier, or null.
173     * @throws SAXException if an error occurs.
174     */
175    public InputSource resolveEntity(String publicId, String systemId)
176            throws SAXException
177    {
178        String resolved = getResolver().getResolvedEntity(publicId, systemId);
179
180        if (resolved != null)
181        {
182            String badFilePrefix = "file://";
183            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                InputStream is = fs.getInputStream(null, resolved);
194                InputSource iSource = new InputSource(resolved);
195                iSource.setPublicId(publicId);
196                iSource.setByteStream(is);
197                return iSource;
198            }
199            catch (Exception e)
200            {
201                log.warn("Failed to create InputSource for " + resolved + " ("
202                                + e.toString() + ")");
203                return null;
204            }
205        }
206
207        return null;
208    }
209
210    /**
211     * Returns the logger used by this configuration object.
212     *
213     * @return the logger
214     */
215    public Log getLogger()
216    {
217        return log;
218    }
219
220    /**
221     * Allows to set the logger to be used by this configuration object. This
222     * method makes it possible for clients to exactly control logging behavior.
223     * Per default a logger is set that will ignore all log messages. Derived
224     * classes that want to enable logging should call this method during their
225     * initialization with the logger to be used.
226     *
227     * @param log the new logger
228     */
229    public void setLogger(Log log)
230    {
231        this.log = (log != null) ? log : LogFactory.getLog(CatalogResolver.class);
232    }
233
234    private synchronized org.apache.xml.resolver.tools.CatalogResolver getResolver()
235    {
236        if (resolver == null)
237        {
238            resolver = new org.apache.xml.resolver.tools.CatalogResolver(manager);
239        }
240        return resolver;
241    }
242
243    /**
244     * Extend the CatalogManager to make the FileSystem and base directory accessible.
245     */
246    public static class CatalogManager extends org.apache.xml.resolver.CatalogManager
247    {
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 String Substitutor */
258        private StrSubstitutor substitutor;
259
260        /**
261         * Set the FileSystem
262         * @param fileSystem The FileSystem in use.
263         */
264        public void setFileSystem(FileSystem fileSystem)
265        {
266            this.fs = fileSystem;
267        }
268
269        /**
270         * Retrieve the FileSystem.
271         * @return The FileSystem.
272         */
273        public FileSystem getFileSystem()
274        {
275            return this.fs;
276        }
277
278        /**
279         * Set the base directory.
280         * @param baseDir The base directory.
281         */
282        public void setBaseDir(String baseDir)
283        {
284            if (baseDir != null)
285            {
286                this.baseDir = baseDir;
287            }
288        }
289
290        /**
291         * Return the base directory.
292         * @return The base directory.
293         */
294        public String getBaseDir()
295        {
296            return this.baseDir;
297        }
298
299        public void setSubstitutor(StrSubstitutor substitutor)
300        {
301            this.substitutor = substitutor;
302        }
303
304        public StrSubstitutor getStrSubstitutor()
305        {
306            return this.substitutor;
307        }
308
309
310        /**
311         * Get a new catalog instance. This method is only overridden because xml-resolver
312         * might be in a parent ClassLoader and will be incapable of loading our Catalog
313         * implementation.
314         *
315         * This method always returns a new instance of the underlying catalog class.
316         * @return the Catalog.
317         */
318        @Override
319        public org.apache.xml.resolver.Catalog getPrivateCatalog()
320        {
321            org.apache.xml.resolver.Catalog catalog = staticCatalog;
322
323            if (catalog == null || !getUseStaticCatalog())
324            {
325                try
326                {
327                    catalog = new Catalog();
328                    catalog.setCatalogManager(this);
329                    catalog.setupReaders();
330                    catalog.loadSystemCatalogs();
331                }
332                catch (Exception ex)
333                {
334                    ex.printStackTrace();
335                }
336
337                if (getUseStaticCatalog())
338                {
339                    staticCatalog = catalog;
340                }
341            }
342
343            return catalog;
344        }
345
346        /**
347         * Get a catalog instance.
348         *
349         * If this manager uses static catalogs, the same static catalog will
350         * always be returned. Otherwise a new catalog will be returned.
351         * @return The Catalog.
352         */
353        @Override
354        public org.apache.xml.resolver.Catalog getCatalog()
355        {
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    {
365        /** The FileSystem */
366        private FileSystem fs;
367
368        /** FileNameMap to determine the mime type */
369        private FileNameMap fileNameMap = URLConnection.getFileNameMap();
370
371        /**
372         * Load the catalogs.
373         * @throws IOException if an error occurs.
374         */
375        @Override
376        public void loadSystemCatalogs() throws IOException
377        {
378            fs = ((CatalogManager) catalogManager).getFileSystem();
379            String base = ((CatalogManager) catalogManager).getBaseDir();
380
381            // This is safe because the catalog manager returns a vector of strings.
382            @SuppressWarnings("unchecked")
383            Vector<String> catalogs = catalogManager.getCatalogFiles();
384            if (catalogs != null)
385            {
386                for (int count = 0; count < catalogs.size(); count++)
387                {
388                    String fileName = catalogs.elementAt(count);
389
390                    URL url = null;
391                    InputStream is = null;
392
393                    try
394                    {
395                        url = ConfigurationUtils.locate(fs, base, fileName);
396                        if (url != null)
397                        {
398                            is = fs.getInputStream(url);
399                        }
400                    }
401                    catch (ConfigurationException ce)
402                    {
403                        String name = (url == null) ? fileName : url.toString();
404                        // Ignore the exception.
405                        catalogManager.debug.message(DEBUG_ALL,
406                            "Unable to get input stream for " + name + ". " + ce.getMessage());
407                    }
408                    if (is != null)
409                    {
410                        String mimeType = fileNameMap.getContentTypeFor(fileName);
411                        try
412                        {
413                            if (mimeType != null)
414                            {
415                                parseCatalog(mimeType, is);
416                                continue;
417                            }
418                        }
419                        catch (Exception ex)
420                        {
421                            // Ignore the exception.
422                            catalogManager.debug.message(DEBUG_ALL,
423                                "Exception caught parsing input stream for " + fileName + ". "
424                                + ex.getMessage());
425                        }
426                        finally
427                        {
428                            is.close();
429                        }
430                    }
431                    parseCatalog(base, fileName);
432                }
433            }
434
435        }
436
437        /**
438         * Parse the specified catalog file.
439         * @param baseDir The base directory, if not included in the file name.
440         * @param fileName The catalog file. May be a full URI String.
441         * @throws IOException If an error occurs.
442         */
443        public void parseCatalog(String baseDir, String fileName) throws IOException
444        {
445            base = ConfigurationUtils.locate(fs, baseDir, fileName);
446            catalogCwd = base;
447            default_override = catalogManager.getPreferPublic();
448            catalogManager.debug.message(DEBUG_NORMAL, "Parse catalog: " + fileName);
449
450            boolean parsed = false;
451
452            for (int count = 0; !parsed && count < readerArr.size(); count++)
453            {
454                CatalogReader reader = (CatalogReader) readerArr.get(count);
455                InputStream inStream;
456
457                try
458                {
459                    inStream = fs.getInputStream(base);
460                }
461                catch (Exception ex)
462                {
463                    catalogManager.debug.message(DEBUG_NORMAL, "Unable to access " + base
464                        + ex.getMessage());
465                    break;
466                }
467
468                try
469                {
470                    reader.readCatalog(this, inStream);
471                    parsed = true;
472                }
473                catch (CatalogException ce)
474                {
475                    catalogManager.debug.message(DEBUG_NORMAL, "Parse failed for " + fileName
476                            + ce.getMessage());
477                    if (ce.getExceptionType() == CatalogException.PARSE_FAILED)
478                    {
479                        break;
480                    }
481                    else
482                    {
483                        // try again!
484                        continue;
485                    }
486                }
487                finally
488                {
489                    try
490                    {
491                        inStream.close();
492                    }
493                    catch (IOException ioe)
494                    {
495                        // Ignore the exception.
496                        inStream = null;
497                    }
498                }
499            }
500
501            if (parsed)
502            {
503                parsePendingCatalogs();
504            }
505        }
506
507        /**
508         * Perform character normalization on a URI reference.
509         *
510         * @param uriref The URI reference
511         * @return The normalized URI reference.
512         */
513        @Override
514        protected String normalizeURI(String uriref)
515        {
516            StrSubstitutor substitutor = ((CatalogManager) catalogManager).getStrSubstitutor();
517            String resolved = substitutor != null ? substitutor.replace(uriref) : uriref;
518            return super.normalizeURI(resolved);
519        }
520    }
521}