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