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 }