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