View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.resolver;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.net.FileNameMap;
22  import java.net.URL;
23  import java.net.URLConnection;
24  import java.util.Vector;
25  
26  import org.apache.commons.configuration2.ex.ConfigurationException;
27  import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
28  import org.apache.commons.configuration2.io.ConfigurationLogger;
29  import org.apache.commons.configuration2.io.FileLocatorUtils;
30  import org.apache.commons.configuration2.io.FileSystem;
31  import org.apache.xml.resolver.CatalogException;
32  import org.apache.xml.resolver.readers.CatalogReader;
33  import org.xml.sax.EntityResolver;
34  import org.xml.sax.InputSource;
35  import org.xml.sax.SAXException;
36  
37  /**
38   * Thin wrapper around xml commons CatalogResolver to allow list of catalogs to be provided.
39   *
40   * @since 1.7
41   */
42  public class CatalogResolver implements EntityResolver {
43      /**
44       * Debug everything.
45       */
46      private static final int DEBUG_ALL = 9;
47  
48      /**
49       * Normal debug setting.
50       */
51      private static final int DEBUG_NORMAL = 4;
52  
53      /**
54       * Debug nothing.
55       */
56      private static final int DEBUG_NONE = 0;
57  
58      /**
59       * The CatalogManager
60       */
61      private final CatalogManager manager = new CatalogManager();
62  
63      /**
64       * The FileSystem in use.
65       */
66      private FileSystem fs = FileLocatorUtils.DEFAULT_FILE_SYSTEM;
67  
68      /**
69       * The CatalogResolver
70       */
71      private org.apache.xml.resolver.tools.CatalogResolver resolver;
72  
73      /**
74       * Stores the logger.
75       */
76      private ConfigurationLogger log;
77  
78      /**
79       * Constructs the CatalogResolver
80       */
81      public CatalogResolver() {
82          manager.setIgnoreMissingProperties(true);
83          manager.setUseStaticCatalog(false);
84          manager.setFileSystem(fs);
85          initLogger(null);
86      }
87  
88      /**
89       * Sets the list of catalog file names
90       *
91       * @param catalogs The delimited list of catalog files.
92       */
93      public void setCatalogFiles(final String catalogs) {
94          manager.setCatalogFiles(catalogs);
95      }
96  
97      /**
98       * Sets the FileSystem.
99       *
100      * @param fileSystem The FileSystem.
101      */
102     public void setFileSystem(final FileSystem fileSystem) {
103         this.fs = fileSystem;
104         manager.setFileSystem(fileSystem);
105     }
106 
107     /**
108      * Sets the base path.
109      *
110      * @param baseDir The base path String.
111      */
112     public void setBaseDir(final String baseDir) {
113         manager.setBaseDir(baseDir);
114     }
115 
116     /**
117      * Sets the {@code ConfigurationInterpolator}.
118      *
119      * @param ci the {@code ConfigurationInterpolator}
120      */
121     public void setInterpolator(final ConfigurationInterpolator ci) {
122         manager.setInterpolator(ci);
123     }
124 
125     /**
126      * Enables debug logging of xml-commons Catalog processing.
127      *
128      * @param debug True if debugging should be enabled, false otherwise.
129      */
130     public void setDebug(final boolean debug) {
131         manager.setVerbosity(debug ? DEBUG_ALL : DEBUG_NONE);
132     }
133 
134     /**
135      * <p>
136      * Implements the {@code resolveEntity} method for the SAX interface.
137      * </p>
138      * <p>
139      * Presented with an optional public identifier and a system identifier, this function attempts to locate a mapping in
140      * the catalogs.
141      * </p>
142      * <p>
143      * If such a mapping is found, the resolver attempts to open the mapped value as an InputSource and return it.
144      * Exceptions are ignored and null is returned if the mapped value cannot be opened as an input source.
145      * </p>
146      * <p>
147      * If no mapping is found (or an error occurs attempting to open the mapped value as an input source), null is returned
148      * and the system will use the specified system identifier as if no entityResolver was specified.
149      * </p>
150      *
151      * @param publicId The public identifier for the entity in question. This may be null.
152      * @param systemId The system identifier for the entity in question. XML requires a system identifier on all external
153      *        entities, so this value is always specified.
154      * @return An InputSource for the mapped identifier, or null.
155      * @throws SAXException if an error occurs.
156      */
157     @SuppressWarnings("resource") // InputSource wraps an InputStream.
158     @Override
159     public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException {
160         String resolved = getResolver().getResolvedEntity(publicId, systemId);
161 
162         if (resolved != null) {
163             final String badFilePrefix = "file://";
164             final String correctFilePrefix = "file:///";
165 
166             // Java 5 has a bug when constructing file URLs
167             if (resolved.startsWith(badFilePrefix) && !resolved.startsWith(correctFilePrefix)) {
168                 resolved = correctFilePrefix + resolved.substring(badFilePrefix.length());
169             }
170 
171             try {
172                 final URL url = locate(fs, null, resolved);
173                 if (url == null) {
174                     throw new ConfigurationException("Could not locate " + resolved);
175                 }
176                 final InputStream inputStream = fs.getInputStream(url);
177                 final InputSource inputSource = new InputSource(resolved);
178                 inputSource.setPublicId(publicId);
179                 inputSource.setByteStream(inputStream);
180                 return inputSource;
181             } catch (final Exception e) {
182                 log.warn("Failed to create InputSource for " + resolved, e);
183             }
184         }
185 
186         return null;
187     }
188 
189     /**
190      * Gets the logger used by this configuration object.
191      *
192      * @return the logger
193      */
194     public ConfigurationLogger getLogger() {
195         return log;
196     }
197 
198     /**
199      * Allows setting the logger to be used by this object. This method makes it possible for clients to exactly control
200      * logging behavior. Per default a logger is set that will ignore all log messages. Derived classes that want to enable
201      * logging should call this method during their initialization with the logger to be used. Passing in <b>null</b> as
202      * argument disables logging.
203      *
204      * @param log the new logger
205      */
206     public void setLogger(final ConfigurationLogger log) {
207         initLogger(log);
208     }
209 
210     /**
211      * Initializes the logger. Checks for null parameters.
212      *
213      * @param log the new logger
214      */
215     private void initLogger(final ConfigurationLogger log) {
216         this.log = log != null ? log : ConfigurationLogger.newDummyLogger();
217     }
218 
219     private synchronized org.apache.xml.resolver.tools.CatalogResolver getResolver() {
220         if (resolver == null) {
221             resolver = new org.apache.xml.resolver.tools.CatalogResolver(manager);
222         }
223         return resolver;
224     }
225 
226     /**
227      * Locates a given file. This implementation delegates to the corresponding method in {@link FileLocatorUtils}.
228      *
229      * @param fs the {@code FileSystem}
230      * @param basePath the base path
231      * @param name the file name
232      * @return the URL pointing to the file
233      */
234     private static URL locate(final FileSystem fs, final String basePath, final String name) {
235         return FileLocatorUtils.locate(FileLocatorUtils.fileLocator().fileSystem(fs).basePath(basePath).fileName(name).create());
236     }
237 
238     /**
239      * Extends the CatalogManager to make the FileSystem and base directory accessible.
240      */
241     public static class CatalogManager extends org.apache.xml.resolver.CatalogManager {
242         /** The static catalog used by this manager. */
243         private static org.apache.xml.resolver.Catalog staticCatalog;
244 
245         /** The FileSystem */
246         private FileSystem fs;
247 
248         /** The base directory */
249         private String baseDir = System.getProperty("user.dir");
250 
251         /** The object for handling interpolation. */
252         private ConfigurationInterpolator interpolator;
253 
254         /**
255          * Sets the FileSystem
256          *
257          * @param fileSystem The FileSystem in use.
258          */
259         public void setFileSystem(final FileSystem fileSystem) {
260             this.fs = fileSystem;
261         }
262 
263         /**
264          * Gets the FileSystem.
265          *
266          * @return The FileSystem.
267          */
268         public FileSystem getFileSystem() {
269             return this.fs;
270         }
271 
272         /**
273          * Sets the base directory.
274          *
275          * @param baseDir The base directory.
276          */
277         public void setBaseDir(final String baseDir) {
278             if (baseDir != null) {
279                 this.baseDir = baseDir;
280             }
281         }
282 
283         /**
284          * Gets the base directory.
285          *
286          * @return The base directory.
287          */
288         public String getBaseDir() {
289             return this.baseDir;
290         }
291 
292         /**
293          * Sets the ConfigurationInterpolator.
294          *
295          * @param configurationInterpolator the ConfigurationInterpolator.
296          */
297         public void setInterpolator(final ConfigurationInterpolator configurationInterpolator) {
298             interpolator = configurationInterpolator;
299         }
300 
301         /**
302          * Gets the ConfigurationInterpolator.
303          *
304          * @return the ConfigurationInterpolator.
305          */
306         public ConfigurationInterpolator getInterpolator() {
307             return interpolator;
308         }
309 
310         /**
311          * Gets a new catalog instance. This method is only overridden because xml-resolver might be in a parent ClassLoader and
312          * will be incapable of loading our Catalog implementation.
313          *
314          * This method always returns a new instance of the underlying catalog class.
315          *
316          * @return the Catalog.
317          */
318         @Override
319         public org.apache.xml.resolver.Catalog getPrivateCatalog() {
320             org.apache.xml.resolver.Catalog catalog = staticCatalog;
321 
322             if (catalog == null || !getUseStaticCatalog()) {
323                 try {
324                     catalog = new Catalog();
325                     catalog.setCatalogManager(this);
326                     catalog.setupReaders();
327                     catalog.loadSystemCatalogs();
328                 } catch (final Exception ex) {
329                     ex.printStackTrace();
330                 }
331 
332                 if (getUseStaticCatalog()) {
333                     staticCatalog = catalog;
334                 }
335             }
336 
337             return catalog;
338         }
339 
340         /**
341          * Gets a catalog instance.
342          *
343          * If this manager uses static catalogs, the same static catalog will always be returned. Otherwise a new catalog will
344          * be returned.
345          *
346          * @return The Catalog.
347          */
348         @Override
349         public org.apache.xml.resolver.Catalog getCatalog() {
350             return getPrivateCatalog();
351         }
352     }
353 
354     /**
355      * Overrides the Catalog implementation to use the underlying FileSystem.
356      */
357     public static class Catalog extends org.apache.xml.resolver.Catalog {
358         /** The FileSystem */
359         private FileSystem fs;
360 
361         /** FileNameMap to determine the mime type */
362         private final FileNameMap fileNameMap = URLConnection.getFileNameMap();
363 
364         /**
365          * Load the catalogs.
366          *
367          * @throws IOException if an error occurs.
368          */
369         @Override
370         public void loadSystemCatalogs() throws IOException {
371             fs = ((CatalogManager) catalogManager).getFileSystem();
372             final String base = ((CatalogManager) catalogManager).getBaseDir();
373 
374             // This is safe because the catalog manager returns a vector of strings.
375             final Vector<String> catalogs = catalogManager.getCatalogFiles();
376             if (catalogs != null) {
377                 for (int count = 0; count < catalogs.size(); count++) {
378                     final String fileName = catalogs.elementAt(count);
379 
380                     URL url = null;
381                     InputStream inputStream = null;
382 
383                     try {
384                         url = locate(fs, base, fileName);
385                         if (url != null) {
386                             inputStream = fs.getInputStream(url);
387                         }
388                     } catch (final ConfigurationException ce) {
389                         final String name = url.toString();
390                         // Ignore the exception.
391                         catalogManager.debug.message(DEBUG_ALL, "Unable to get input stream for " + name + ". " + ce.getMessage());
392                     }
393                     if (inputStream != null) {
394                         final String mimeType = fileNameMap.getContentTypeFor(fileName);
395                         try {
396                             if (mimeType != null) {
397                                 parseCatalog(mimeType, inputStream);
398                                 continue;
399                             }
400                         } catch (final Exception ex) {
401                             // Ignore the exception.
402                             catalogManager.debug.message(DEBUG_ALL, "Exception caught parsing input stream for " + fileName + ". " + ex.getMessage());
403                         } finally {
404                             inputStream.close();
405                         }
406                     }
407                     parseCatalog(base, fileName);
408                 }
409             }
410 
411         }
412 
413         /**
414          * Parses the specified catalog file.
415          *
416          * @param baseDir The base directory, if not included in the file name.
417          * @param fileName The catalog file. May be a full URI String.
418          * @throws IOException If an error occurs.
419          */
420         public void parseCatalog(final String baseDir, final String fileName) throws IOException {
421             base = locate(fs, baseDir, fileName);
422             catalogCwd = base;
423             default_override = catalogManager.getPreferPublic();
424             catalogManager.debug.message(DEBUG_NORMAL, "Parse catalog: " + fileName);
425 
426             boolean parsed = false;
427 
428             for (int count = 0; !parsed && count < readerArr.size(); count++) {
429                 final CatalogReader reader = (CatalogReader) readerArr.get(count);
430                 InputStream inputStream;
431 
432                 try {
433                     inputStream = fs.getInputStream(base);
434                 } catch (final Exception ex) {
435                     catalogManager.debug.message(DEBUG_NORMAL, "Unable to access " + base + ex.getMessage());
436                     break;
437                 }
438 
439                 try {
440                     reader.readCatalog(this, inputStream);
441                     parsed = true;
442                 } catch (final CatalogException ce) {
443                     catalogManager.debug.message(DEBUG_NORMAL, "Parse failed for " + fileName + ce.getMessage());
444                     if (ce.getExceptionType() == CatalogException.PARSE_FAILED) {
445                         break;
446                     }
447                     // try again!
448                     continue;
449                 } finally {
450                     try {
451                         inputStream.close();
452                     } catch (final IOException ioe) {
453                         // Ignore the exception.
454                         inputStream = null;
455                     }
456                 }
457             }
458 
459             if (parsed) {
460                 parsePendingCatalogs();
461             }
462         }
463 
464         /**
465          * Performs character normalization on a URI reference.
466          *
467          * @param uriref The URI reference
468          * @return The normalized URI reference.
469          */
470         @Override
471         protected String normalizeURI(final String uriref) {
472             final ConfigurationInterpolator ci = ((CatalogManager) catalogManager).getInterpolator();
473             final String resolved = ci != null ? String.valueOf(ci.interpolate(uriref)) : uriref;
474             return super.normalizeURI(resolved);
475         }
476     }
477 }