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 * https://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.ex.ConfigurationException; 027import org.apache.commons.configuration2.interpol.ConfigurationInterpolator; 028import org.apache.commons.configuration2.io.ConfigurationLogger; 029import org.apache.commons.configuration2.io.FileLocatorUtils; 030import org.apache.commons.configuration2.io.FileSystem; 031import org.apache.commons.lang3.SystemProperties; 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 /** 046 * Overrides the Catalog implementation to use the underlying FileSystem. 047 */ 048 public static class Catalog extends org.apache.xml.resolver.Catalog { 049 050 /** The FileSystem */ 051 private FileSystem fs; 052 053 /** FileNameMap to determine the mime type */ 054 private final FileNameMap fileNameMap = URLConnection.getFileNameMap(); 055 056 /** 057 * Constructs a new instance. 058 */ 059 public Catalog() { 060 // empty 061 } 062 063 /** 064 * Load the catalogs. 065 * 066 * @throws IOException if an error occurs. 067 */ 068 @Override 069 public void loadSystemCatalogs() throws IOException { 070 fs = ((CatalogManager) catalogManager).getFileSystem(); 071 final String base = ((CatalogManager) catalogManager).getBaseDir(); 072 073 // This is safe because the catalog manager returns a vector of strings. 074 final Vector<String> catalogs = catalogManager.getCatalogFiles(); 075 if (catalogs != null) { 076 for (int count = 0; count < catalogs.size(); count++) { 077 final String fileName = catalogs.elementAt(count); 078 079 URL url = null; 080 InputStream inputStream = null; 081 082 try { 083 url = locate(fs, base, fileName); 084 if (url != null) { 085 inputStream = fs.getInputStream(url); 086 } 087 } catch (final ConfigurationException ce) { 088 final String name = url.toString(); 089 // Ignore the exception. 090 catalogManager.debug.message(DEBUG_ALL, "Unable to get input stream for " + name + ". " + ce.getMessage()); 091 } 092 if (inputStream != null) { 093 final String mimeType = fileNameMap.getContentTypeFor(fileName); 094 try { 095 if (mimeType != null) { 096 parseCatalog(mimeType, inputStream); 097 continue; 098 } 099 } 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}