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