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     */
017    package org.apache.commons.configuration.resolver;
018    
019    import java.io.IOException;
020    import java.io.InputStream;
021    import java.net.FileNameMap;
022    import java.net.URL;
023    import java.net.URLConnection;
024    import java.util.Vector;
025    
026    import org.apache.commons.configuration.ConfigurationException;
027    import org.apache.commons.configuration.ConfigurationUtils;
028    import org.apache.commons.configuration.FileSystem;
029    import org.apache.commons.lang.text.StrSubstitutor;
030    import org.apache.commons.logging.Log;
031    import org.apache.commons.logging.LogFactory;
032    import org.apache.xml.resolver.CatalogException;
033    import org.apache.xml.resolver.readers.CatalogReader;
034    import org.xml.sax.EntityResolver;
035    import org.xml.sax.InputSource;
036    import 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 1206764 2011-11-27 16:45:39Z oheger $
046     */
047    public 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 = (String) 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    }