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