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 }