001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.configuration.resolver;
018
019 import java.io.IOException;
020 import java.io.InputStream;
021 import java.net.FileNameMap;
022 import java.net.URL;
023 import java.net.URLConnection;
024 import java.util.Vector;
025
026 import org.apache.commons.configuration.ConfigurationException;
027 import org.apache.commons.configuration.ConfigurationUtils;
028 import org.apache.commons.configuration.FileSystem;
029 import org.apache.commons.lang.text.StrSubstitutor;
030 import org.apache.commons.logging.Log;
031 import org.apache.commons.logging.LogFactory;
032 import org.apache.xml.resolver.CatalogException;
033 import org.apache.xml.resolver.readers.CatalogReader;
034 import org.xml.sax.EntityResolver;
035 import org.xml.sax.InputSource;
036 import org.xml.sax.SAXException;
037
038 /**
039 * Thin wrapper around xml commons CatalogResolver to allow list of catalogs
040 * to be provided.
041 * @author <a
042 * href="http://commons.apache.org/configuration/team-list.html">Commons
043 * Configuration team</a>
044 * @since 1.7
045 * @version $Id: CatalogResolver.java 1206764 2011-11-27 16:45:39Z oheger $
046 */
047 public class CatalogResolver implements EntityResolver
048 {
049 /**
050 * Debug everything.
051 */
052 private static final int DEBUG_ALL = 9;
053
054 /**
055 * Normal debug setting.
056 */
057 private static final int DEBUG_NORMAL = 4;
058
059 /**
060 * Debug nothing.
061 */
062 private static final int DEBUG_NONE = 0;
063
064 /**
065 * The CatalogManager
066 */
067 protected CatalogManager manager = new CatalogManager();
068
069 /**
070 * The FileSystem in use.
071 */
072 protected FileSystem fs = FileSystem.getDefaultFileSystem();
073
074 /**
075 * The CatalogResolver
076 */
077 private org.apache.xml.resolver.tools.CatalogResolver resolver;
078
079 /**
080 * Stores the logger.
081 */
082 private Log log;
083
084 /**
085 * Constructs the CatalogResolver
086 */
087 public CatalogResolver()
088 {
089 manager.setIgnoreMissingProperties(true);
090 manager.setUseStaticCatalog(false);
091 manager.setFileSystem(fs);
092 setLogger(null);
093 }
094
095 /**
096 * Set the list of catalog file names
097 *
098 * @param catalogs The delimited list of catalog files.
099 */
100 public void setCatalogFiles(String catalogs)
101 {
102 manager.setCatalogFiles(catalogs);
103 }
104
105 /**
106 * Set the FileSystem.
107 * @param fileSystem The FileSystem.
108 */
109 public void setFileSystem(FileSystem fileSystem)
110 {
111 this.fs = fileSystem;
112 manager.setFileSystem(fileSystem);
113 }
114
115 /**
116 * Set the base path.
117 * @param baseDir The base path String.
118 */
119 public void setBaseDir(String baseDir)
120 {
121 manager.setBaseDir(baseDir);
122 }
123
124 /**
125 * Set the StrSubstitutor.
126 * @param substitutor The StrSubstitutor.
127 */
128 public void setSubstitutor(StrSubstitutor substitutor)
129 {
130 manager.setSubstitutor(substitutor);
131 }
132
133 /**
134 * Enables debug logging of xml-commons Catalog processing.
135 * @param debug True if debugging should be enabled, false otherwise.
136 */
137 public void setDebug(boolean debug)
138 {
139 if (debug)
140 {
141 manager.setVerbosity(DEBUG_ALL);
142 }
143 else
144 {
145 manager.setVerbosity(DEBUG_NONE);
146 }
147 }
148
149 /**
150 * Implements the {@code resolveEntity} method
151 * for the SAX interface.
152 * <p/>
153 * <p>Presented with an optional public identifier and a system
154 * identifier, this function attempts to locate a mapping in the
155 * catalogs.</p>
156 * <p/>
157 * <p>If such a mapping is found, the resolver attempts to open
158 * the mapped value as an InputSource and return it. Exceptions are
159 * ignored and null is returned if the mapped value cannot be opened
160 * as an input source.</p>
161 * <p/>
162 * <p>If no mapping is found (or an error occurs attempting to open
163 * the mapped value as an input source), null is returned and the system
164 * will use the specified system identifier as if no entityResolver
165 * was specified.</p>
166 *
167 * @param publicId The public identifier for the entity in question.
168 * This may be null.
169 * @param systemId The system identifier for the entity in question.
170 * XML requires a system identifier on all external entities, so this
171 * value is always specified.
172 * @return An InputSource for the mapped identifier, or null.
173 * @throws SAXException if an error occurs.
174 */
175 public InputSource resolveEntity(String publicId, String systemId)
176 throws SAXException
177 {
178 String resolved = getResolver().getResolvedEntity(publicId, systemId);
179
180 if (resolved != null)
181 {
182 String badFilePrefix = "file://";
183 String correctFilePrefix = "file:///";
184
185 // Java 5 has a bug when constructing file URLS
186 if (resolved.startsWith(badFilePrefix) && !resolved.startsWith(correctFilePrefix))
187 {
188 resolved = correctFilePrefix + resolved.substring(badFilePrefix.length());
189 }
190
191 try
192 {
193 InputStream is = fs.getInputStream(null, resolved);
194 InputSource iSource = new InputSource(resolved);
195 iSource.setPublicId(publicId);
196 iSource.setByteStream(is);
197 return iSource;
198 }
199 catch (Exception e)
200 {
201 log.warn("Failed to create InputSource for " + resolved + " ("
202 + e.toString() + ")");
203 return null;
204 }
205 }
206
207 return null;
208 }
209
210 /**
211 * Returns the logger used by this configuration object.
212 *
213 * @return the logger
214 */
215 public Log getLogger()
216 {
217 return log;
218 }
219
220 /**
221 * Allows to set the logger to be used by this configuration object. This
222 * method makes it possible for clients to exactly control logging behavior.
223 * Per default a logger is set that will ignore all log messages. Derived
224 * classes that want to enable logging should call this method during their
225 * initialization with the logger to be used.
226 *
227 * @param log the new logger
228 */
229 public void setLogger(Log log)
230 {
231 this.log = (log != null) ? log : LogFactory.getLog(CatalogResolver.class);
232 }
233
234 private synchronized org.apache.xml.resolver.tools.CatalogResolver getResolver()
235 {
236 if (resolver == null)
237 {
238 resolver = new org.apache.xml.resolver.tools.CatalogResolver(manager);
239 }
240 return resolver;
241 }
242
243 /**
244 * Extend the CatalogManager to make the FileSystem and base directory accessible.
245 */
246 public static class CatalogManager extends org.apache.xml.resolver.CatalogManager
247 {
248 /** The static catalog used by this manager. */
249 private static org.apache.xml.resolver.Catalog staticCatalog;
250
251 /** The FileSystem */
252 private FileSystem fs;
253
254 /** The base directory */
255 private String baseDir = System.getProperty("user.dir");
256
257 /** The String Substitutor */
258 private StrSubstitutor substitutor;
259
260 /**
261 * Set the FileSystem
262 * @param fileSystem The FileSystem in use.
263 */
264 public void setFileSystem(FileSystem fileSystem)
265 {
266 this.fs = fileSystem;
267 }
268
269 /**
270 * Retrieve the FileSystem.
271 * @return The FileSystem.
272 */
273 public FileSystem getFileSystem()
274 {
275 return this.fs;
276 }
277
278 /**
279 * Set the base directory.
280 * @param baseDir The base directory.
281 */
282 public void setBaseDir(String baseDir)
283 {
284 if (baseDir != null)
285 {
286 this.baseDir = baseDir;
287 }
288 }
289
290 /**
291 * Return the base directory.
292 * @return The base directory.
293 */
294 public String getBaseDir()
295 {
296 return this.baseDir;
297 }
298
299 public void setSubstitutor(StrSubstitutor substitutor)
300 {
301 this.substitutor = substitutor;
302 }
303
304 public StrSubstitutor getStrSubstitutor()
305 {
306 return this.substitutor;
307 }
308
309
310 /**
311 * Get a new catalog instance. This method is only overridden because xml-resolver
312 * might be in a parent ClassLoader and will be incapable of loading our Catalog
313 * implementation.
314 *
315 * This method always returns a new instance of the underlying catalog class.
316 * @return the Catalog.
317 */
318 @Override
319 public org.apache.xml.resolver.Catalog getPrivateCatalog()
320 {
321 org.apache.xml.resolver.Catalog catalog = staticCatalog;
322
323 if (catalog == null || !getUseStaticCatalog())
324 {
325 try
326 {
327 catalog = new Catalog();
328 catalog.setCatalogManager(this);
329 catalog.setupReaders();
330 catalog.loadSystemCatalogs();
331 }
332 catch (Exception ex)
333 {
334 ex.printStackTrace();
335 }
336
337 if (getUseStaticCatalog())
338 {
339 staticCatalog = catalog;
340 }
341 }
342
343 return catalog;
344 }
345
346 /**
347 * Get a catalog instance.
348 *
349 * If this manager uses static catalogs, the same static catalog will
350 * always be returned. Otherwise a new catalog will be returned.
351 * @return The Catalog.
352 */
353 @Override
354 public org.apache.xml.resolver.Catalog getCatalog()
355 {
356 return getPrivateCatalog();
357 }
358 }
359
360 /**
361 * Overrides the Catalog implementation to use the underlying FileSystem.
362 */
363 public static class Catalog extends org.apache.xml.resolver.Catalog
364 {
365 /** The FileSystem */
366 private FileSystem fs;
367
368 /** FileNameMap to determine the mime type */
369 private FileNameMap fileNameMap = URLConnection.getFileNameMap();
370
371 /**
372 * Load the catalogs.
373 * @throws IOException if an error occurs.
374 */
375 @Override
376 public void loadSystemCatalogs() throws IOException
377 {
378 fs = ((CatalogManager) catalogManager).getFileSystem();
379 String base = ((CatalogManager) catalogManager).getBaseDir();
380
381 // This is safe because the catalog manager returns a vector of strings.
382 @SuppressWarnings("unchecked")
383 Vector<String> catalogs = catalogManager.getCatalogFiles();
384 if (catalogs != null)
385 {
386 for (int count = 0; count < catalogs.size(); count++)
387 {
388 String fileName = (String) catalogs.elementAt(count);
389
390 URL url = null;
391 InputStream is = null;
392
393 try
394 {
395 url = ConfigurationUtils.locate(fs, base, fileName);
396 if (url != null)
397 {
398 is = fs.getInputStream(url);
399 }
400 }
401 catch (ConfigurationException ce)
402 {
403 String name = (url == null) ? fileName : url.toString();
404 // Ignore the exception.
405 catalogManager.debug.message(DEBUG_ALL,
406 "Unable to get input stream for " + name + ". " + ce.getMessage());
407 }
408 if (is != null)
409 {
410 String mimeType = fileNameMap.getContentTypeFor(fileName);
411 try
412 {
413 if (mimeType != null)
414 {
415 parseCatalog(mimeType, is);
416 continue;
417 }
418 }
419 catch (Exception ex)
420 {
421 // Ignore the exception.
422 catalogManager.debug.message(DEBUG_ALL,
423 "Exception caught parsing input stream for " + fileName + ". "
424 + ex.getMessage());
425 }
426 finally
427 {
428 is.close();
429 }
430 }
431 parseCatalog(base, fileName);
432 }
433 }
434
435 }
436
437 /**
438 * Parse the specified catalog file.
439 * @param baseDir The base directory, if not included in the file name.
440 * @param fileName The catalog file. May be a full URI String.
441 * @throws IOException If an error occurs.
442 */
443 public void parseCatalog(String baseDir, String fileName) throws IOException
444 {
445 base = ConfigurationUtils.locate(fs, baseDir, fileName);
446 catalogCwd = base;
447 default_override = catalogManager.getPreferPublic();
448 catalogManager.debug.message(DEBUG_NORMAL, "Parse catalog: " + fileName);
449
450 boolean parsed = false;
451
452 for (int count = 0; !parsed && count < readerArr.size(); count++)
453 {
454 CatalogReader reader = (CatalogReader) readerArr.get(count);
455 InputStream inStream;
456
457 try
458 {
459 inStream = fs.getInputStream(base);
460 }
461 catch (Exception ex)
462 {
463 catalogManager.debug.message(DEBUG_NORMAL, "Unable to access " + base
464 + ex.getMessage());
465 break;
466 }
467
468 try
469 {
470 reader.readCatalog(this, inStream);
471 parsed = true;
472 }
473 catch (CatalogException ce)
474 {
475 catalogManager.debug.message(DEBUG_NORMAL, "Parse failed for " + fileName
476 + ce.getMessage());
477 if (ce.getExceptionType() == CatalogException.PARSE_FAILED)
478 {
479 break;
480 }
481 else
482 {
483 // try again!
484 continue;
485 }
486 }
487 finally
488 {
489 try
490 {
491 inStream.close();
492 }
493 catch (IOException ioe)
494 {
495 // Ignore the exception.
496 inStream = null;
497 }
498 }
499 }
500
501 if (parsed)
502 {
503 parsePendingCatalogs();
504 }
505 }
506
507 /**
508 * Perform character normalization on a URI reference.
509 *
510 * @param uriref The URI reference
511 * @return The normalized URI reference.
512 */
513 @Override
514 protected String normalizeURI(String uriref)
515 {
516 StrSubstitutor substitutor = ((CatalogManager) catalogManager).getStrSubstitutor();
517 String resolved = substitutor != null ? substitutor.replace(uriref) : uriref;
518 return super.normalizeURI(resolved);
519 }
520 }
521 }