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