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