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