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