CatalogResolver.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.configuration2.resolver;
import java.io.IOException;
import java.io.InputStream;
import java.net.FileNameMap;
import java.net.URL;
import java.net.URLConnection;
import java.util.Vector;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
import org.apache.commons.configuration2.io.ConfigurationLogger;
import org.apache.commons.configuration2.io.FileLocatorUtils;
import org.apache.commons.configuration2.io.FileSystem;
import org.apache.commons.lang3.SystemProperties;
import org.apache.xml.resolver.CatalogException;
import org.apache.xml.resolver.readers.CatalogReader;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Thin wrapper around xml commons CatalogResolver to allow list of catalogs to be provided.
*
* @since 1.7
*/
public class CatalogResolver implements EntityResolver {
/**
* Overrides the Catalog implementation to use the underlying FileSystem.
*/
public static class Catalog extends org.apache.xml.resolver.Catalog {
/** The FileSystem */
private FileSystem fs;
/** FileNameMap to determine the mime type */
private final FileNameMap fileNameMap = URLConnection.getFileNameMap();
/**
* Load the catalogs.
*
* @throws IOException if an error occurs.
*/
@Override
public void loadSystemCatalogs() throws IOException {
fs = ((CatalogManager) catalogManager).getFileSystem();
final String base = ((CatalogManager) catalogManager).getBaseDir();
// This is safe because the catalog manager returns a vector of strings.
final Vector<String> catalogs = catalogManager.getCatalogFiles();
if (catalogs != null) {
for (int count = 0; count < catalogs.size(); count++) {
final String fileName = catalogs.elementAt(count);
URL url = null;
InputStream inputStream = null;
try {
url = locate(fs, base, fileName);
if (url != null) {
inputStream = fs.getInputStream(url);
}
} catch (final ConfigurationException ce) {
final String name = url.toString();
// Ignore the exception.
catalogManager.debug.message(DEBUG_ALL, "Unable to get input stream for " + name + ". " + ce.getMessage());
}
if (inputStream != null) {
final String mimeType = fileNameMap.getContentTypeFor(fileName);
try {
if (mimeType != null) {
parseCatalog(mimeType, inputStream);
continue;
}
} catch (final Exception ex) {
// Ignore the exception.
catalogManager.debug.message(DEBUG_ALL, "Exception caught parsing input stream for " + fileName + ". " + ex.getMessage());
} finally {
inputStream.close();
}
}
parseCatalog(base, fileName);
}
}
}
/**
* Performs character normalization on a URI reference.
*
* @param uriref The URI reference
* @return The normalized URI reference.
*/
@Override
protected String normalizeURI(final String uriref) {
final ConfigurationInterpolator ci = ((CatalogManager) catalogManager).getInterpolator();
final String resolved = ci != null ? String.valueOf(ci.interpolate(uriref)) : uriref;
return super.normalizeURI(resolved);
}
/**
* Parses the specified catalog file.
*
* @param baseDir The base directory, if not included in the file name.
* @param fileName The catalog file. May be a full URI String.
* @throws IOException If an error occurs.
*/
public void parseCatalog(final String baseDir, final String fileName) throws IOException {
base = locate(fs, baseDir, fileName);
catalogCwd = base;
default_override = catalogManager.getPreferPublic();
catalogManager.debug.message(DEBUG_NORMAL, "Parse catalog: " + fileName);
boolean parsed = false;
for (int count = 0; !parsed && count < readerArr.size(); count++) {
final CatalogReader reader = (CatalogReader) readerArr.get(count);
InputStream inputStream;
try {
inputStream = fs.getInputStream(base);
} catch (final Exception ex) {
catalogManager.debug.message(DEBUG_NORMAL, "Unable to access " + base + ex.getMessage());
break;
}
try {
reader.readCatalog(this, inputStream);
parsed = true;
} catch (final CatalogException ce) {
catalogManager.debug.message(DEBUG_NORMAL, "Parse failed for " + fileName + ce.getMessage());
if (ce.getExceptionType() == CatalogException.PARSE_FAILED) {
break;
}
// try again!
continue;
} finally {
try {
inputStream.close();
} catch (final IOException ioe) {
// Ignore the exception.
inputStream = null;
}
}
}
if (parsed) {
parsePendingCatalogs();
}
}
}
/**
* Extends the CatalogManager to make the FileSystem and base directory accessible.
*/
public static class CatalogManager extends org.apache.xml.resolver.CatalogManager {
/** The static catalog used by this manager. */
private static org.apache.xml.resolver.Catalog staticCatalog;
/** The FileSystem */
private FileSystem fs;
/** The base directory */
private String baseDir = SystemProperties.getUserDir();
/** The object for handling interpolation. */
private ConfigurationInterpolator interpolator;
/**
* Gets the base directory.
*
* @return The base directory.
*/
public String getBaseDir() {
return this.baseDir;
}
/**
* Gets a catalog instance.
*
* If this manager uses static catalogs, the same static catalog will always be returned. Otherwise a new catalog will
* be returned.
*
* @return The Catalog.
*/
@Override
public org.apache.xml.resolver.Catalog getCatalog() {
return getPrivateCatalog();
}
/**
* Gets the FileSystem.
*
* @return The FileSystem.
*/
public FileSystem getFileSystem() {
return this.fs;
}
/**
* Gets the ConfigurationInterpolator.
*
* @return the ConfigurationInterpolator.
*/
public ConfigurationInterpolator getInterpolator() {
return interpolator;
}
/**
* Gets a new catalog instance. This method is only overridden because xml-resolver might be in a parent ClassLoader and
* will be incapable of loading our Catalog implementation.
*
* This method always returns a new instance of the underlying catalog class.
*
* @return the Catalog.
*/
@Override
public org.apache.xml.resolver.Catalog getPrivateCatalog() {
org.apache.xml.resolver.Catalog catalog = staticCatalog;
if (catalog == null || !getUseStaticCatalog()) {
try {
catalog = new Catalog();
catalog.setCatalogManager(this);
catalog.setupReaders();
catalog.loadSystemCatalogs();
} catch (final Exception ex) {
ex.printStackTrace();
}
if (getUseStaticCatalog()) {
staticCatalog = catalog;
}
}
return catalog;
}
/**
* Sets the base directory.
*
* @param baseDir The base directory.
*/
public void setBaseDir(final String baseDir) {
if (baseDir != null) {
this.baseDir = baseDir;
}
}
/**
* Sets the FileSystem
*
* @param fileSystem The FileSystem in use.
*/
public void setFileSystem(final FileSystem fileSystem) {
this.fs = fileSystem;
}
/**
* Sets the ConfigurationInterpolator.
*
* @param configurationInterpolator the ConfigurationInterpolator.
*/
public void setInterpolator(final ConfigurationInterpolator configurationInterpolator) {
interpolator = configurationInterpolator;
}
}
/**
* Debug everything.
*/
private static final int DEBUG_ALL = 9;
/**
* Normal debug setting.
*/
private static final int DEBUG_NORMAL = 4;
/**
* Debug nothing.
*/
private static final int DEBUG_NONE = 0;
/**
* Locates a given file. This implementation delegates to the corresponding method in {@link FileLocatorUtils}.
*
* @param fs the {@code FileSystem}
* @param basePath the base path
* @param name the file name
* @return the URL pointing to the file
*/
private static URL locate(final FileSystem fs, final String basePath, final String name) {
return FileLocatorUtils.locate(FileLocatorUtils.fileLocator().fileSystem(fs).basePath(basePath).fileName(name).create());
}
/**
* The CatalogManager
*/
private final CatalogManager manager = new CatalogManager();
/**
* The FileSystem in use.
*/
private FileSystem fs = FileLocatorUtils.DEFAULT_FILE_SYSTEM;
/**
* The CatalogResolver
*/
private org.apache.xml.resolver.tools.CatalogResolver resolver;
/**
* Stores the logger.
*/
private ConfigurationLogger log;
/**
* Constructs the CatalogResolver
*/
public CatalogResolver() {
manager.setIgnoreMissingProperties(true);
manager.setUseStaticCatalog(false);
manager.setFileSystem(fs);
initLogger(null);
}
/**
* Gets the logger used by this configuration object.
*
* @return the logger
*/
public ConfigurationLogger getLogger() {
return log;
}
private synchronized org.apache.xml.resolver.tools.CatalogResolver getResolver() {
if (resolver == null) {
resolver = new org.apache.xml.resolver.tools.CatalogResolver(manager);
}
return resolver;
}
/**
* Initializes the logger. Checks for null parameters.
*
* @param log the new logger
*/
private void initLogger(final ConfigurationLogger log) {
this.log = log != null ? log : ConfigurationLogger.newDummyLogger();
}
/**
* <p>
* Implements the {@code resolveEntity} method for the SAX interface.
* </p>
* <p>
* Presented with an optional public identifier and a system identifier, this function attempts to locate a mapping in
* the catalogs.
* </p>
* <p>
* If such a mapping is found, the resolver attempts to open the mapped value as an InputSource and return it.
* Exceptions are ignored and null is returned if the mapped value cannot be opened as an input source.
* </p>
* <p>
* If no mapping is found (or an error occurs attempting to open the mapped value as an input source), null is returned
* and the system will use the specified system identifier as if no entityResolver was specified.
* </p>
*
* @param publicId The public identifier for the entity in question. This may be null.
* @param systemId The system identifier for the entity in question. XML requires a system identifier on all external
* entities, so this value is always specified.
* @return An InputSource for the mapped identifier, or null.
* @throws SAXException if an error occurs.
*/
@SuppressWarnings("resource") // InputSource wraps an InputStream.
@Override
public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException {
String resolved = getResolver().getResolvedEntity(publicId, systemId);
if (resolved != null) {
final String badFilePrefix = "file://";
final String correctFilePrefix = "file:///";
// Java 5 has a bug when constructing file URLs
if (resolved.startsWith(badFilePrefix) && !resolved.startsWith(correctFilePrefix)) {
resolved = correctFilePrefix + resolved.substring(badFilePrefix.length());
}
try {
final URL url = locate(fs, null, resolved);
if (url == null) {
throw new ConfigurationException("Could not locate " + resolved);
}
final InputStream inputStream = fs.getInputStream(url);
final InputSource inputSource = new InputSource(resolved);
inputSource.setPublicId(publicId);
inputSource.setByteStream(inputStream);
return inputSource;
} catch (final Exception e) {
log.warn("Failed to create InputSource for " + resolved, e);
}
}
return null;
}
/**
* Sets the base path.
*
* @param baseDir The base path String.
*/
public void setBaseDir(final String baseDir) {
manager.setBaseDir(baseDir);
}
/**
* Sets the list of catalog file names
*
* @param catalogs The delimited list of catalog files.
*/
public void setCatalogFiles(final String catalogs) {
manager.setCatalogFiles(catalogs);
}
/**
* Enables debug logging of xml-commons Catalog processing.
*
* @param debug True if debugging should be enabled, false otherwise.
*/
public void setDebug(final boolean debug) {
manager.setVerbosity(debug ? DEBUG_ALL : DEBUG_NONE);
}
/**
* Sets the FileSystem.
*
* @param fileSystem The FileSystem.
*/
public void setFileSystem(final FileSystem fileSystem) {
this.fs = fileSystem;
manager.setFileSystem(fileSystem);
}
/**
* Sets the {@code ConfigurationInterpolator}.
*
* @param ci the {@code ConfigurationInterpolator}
*/
public void setInterpolator(final ConfigurationInterpolator ci) {
manager.setInterpolator(ci);
}
/**
* Allows setting the logger to be used by this object. This method makes it possible for clients to exactly control
* logging behavior. Per default a logger is set that will ignore all log messages. Derived classes that want to enable
* logging should call this method during their initialization with the logger to be used. Passing in <b>null</b> as
* argument disables logging.
*
* @param log the new logger
*/
public void setLogger(final ConfigurationLogger log) {
initLogger(log);
}
}