blob: 1a1d3415e326574a1eb3186b552e35bd3f2c75da [file] [log] [blame]
/*
* 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.
*/
/* $Id$ */
package org.apache.xmlgraphics.image.loader.impl;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.image.loader.ImageSessionContext;
import org.apache.xmlgraphics.image.loader.ImageSource;
import org.apache.xmlgraphics.image.loader.util.ImageUtil;
import org.apache.xmlgraphics.image.loader.util.SoftMapCache;
import org.apache.xmlgraphics.io.XmlSourceUtil;
/**
* Abstract base class for classes implementing ImageSessionContext. This class provides all the
* special treatment for Source creation, i.e. it provides optimized Source objects where possible.
*/
public abstract class AbstractImageSessionContext implements ImageSessionContext {
/** logger */
private static final Log log = LogFactory.getLog(AbstractImageSessionContext.class);
private static boolean noSourceReuse;
static {
noSourceReuse = AccessController.doPrivileged(
new PrivilegedAction<Boolean>() {
public Boolean run() {
//See: http://markmail.org/message/k6mno3jsxmovaz2e
String noSourceReuseString = System.getProperty(
AbstractImageSessionContext.class.getName() + ".no-source-reuse");
return Boolean.valueOf(noSourceReuseString);
}
}
);
}
private final FallbackResolver fallbackResolver;
public AbstractImageSessionContext() {
fallbackResolver = new UnrestrictedFallbackResolver();
}
/**
* @param fallbackResolver the fallback resolution mechanism to be used when simply getting an
* {@link InputStream} that backs a {@link Source} isn't possible.
*/
public AbstractImageSessionContext(FallbackResolver fallbackResolver) {
this.fallbackResolver = fallbackResolver;
}
/**
* Attempts to resolve the given URI.
* @param uri URI to access
* @return A {@link javax.xml.transform.Source} object, or null if the URI
* cannot be resolved.
*/
protected abstract Source resolveURI(String uri);
/** {@inheritDoc} */
public Source newSource(String uri) {
Source source = resolveURI(uri);
if (source instanceof StreamSource || source instanceof SAXSource) {
return fallbackResolver.createSource(source, uri);
}
//Return any non-stream Sources and let the ImageLoaders deal with them
return source;
}
protected static ImageInputStream createImageInputStream(InputStream in) throws IOException {
ImageInputStream iin = ImageIO.createImageInputStream(in);
return (ImageInputStream) Proxy.newProxyInstance(
ImageInputStream.class.getClassLoader(),
new Class[] {ImageInputStream.class},
new ObservingImageInputStreamInvocationHandler(iin, in));
}
private static class ObservingImageInputStreamInvocationHandler
implements InvocationHandler {
private ImageInputStream iin;
private InputStream in;
public ObservingImageInputStreamInvocationHandler(ImageInputStream iin,
InputStream underlyingStream) {
this.iin = iin;
this.in = underlyingStream;
}
/** {@inheritDoc} */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if ("close".equals(method.getName())) {
try {
return method.invoke(iin, args);
} finally {
IOUtils.closeQuietly(this.in);
this.in = null;
}
} else {
return method.invoke(iin, args);
}
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
}
/**
* Convert from a <code>URL</code> to a <code>File</code>.
* <p>
* This method will decode the URL.
* Syntax such as <code>file:///my%20docs/file.txt</code> will be
* correctly decoded to <code>/my docs/file.txt</code>.
* <p>
* Note: this method has been copied over from Apache Commons IO and enhanced to support
* UNC paths.
*
* @param url the file URL to convert, <code>null</code> returns <code>null</code>
* @return the equivalent <code>File</code> object, or <code>null</code>
* if the URL's protocol is not <code>file</code>
* @throws IllegalArgumentException if the file is incorrectly encoded
*/
public static File toFile(URL url) {
if (url == null || !url.getProtocol().equals("file")) {
return null;
} else {
try {
String filename = "";
if (url.getHost() != null && url.getHost().length() > 0) {
filename += Character.toString(File.separatorChar)
+ Character.toString(File.separatorChar)
+ url.getHost();
}
filename += url.getFile().replace('/', File.separatorChar);
filename = java.net.URLDecoder.decode(filename, "UTF-8");
final File f = new File(filename);
if (!f.isFile()) {
return null;
}
return f;
} catch (java.io.UnsupportedEncodingException uee) {
assert false;
return null;
}
}
}
private SoftMapCache sessionSources = new SoftMapCache(false); //no need for synchronization
/** {@inheritDoc} */
public Source getSource(String uri) {
return (Source) sessionSources.remove(uri);
}
/** {@inheritDoc} */
public Source needSource(String uri) throws FileNotFoundException {
Source src = getSource(uri);
if (src == null) {
if (log.isDebugEnabled()) {
log.debug("Creating new Source for " + uri);
}
src = newSource(uri);
if (src == null) {
throw new FileNotFoundException("Image not found: " + uri);
}
} else {
if (log.isDebugEnabled()) {
log.debug("Reusing Source for " + uri);
}
}
return src;
}
/** {@inheritDoc} */
public void returnSource(String uri, Source src) {
//Safety check to make sure the Preloaders behave
ImageInputStream in = ImageUtil.getImageInputStream(src);
try {
if (in != null && in.getStreamPosition() != 0) {
throw new IllegalStateException("ImageInputStream is not reset for: " + uri);
}
} catch (IOException ioe) {
//Ignore exception
XmlSourceUtil.closeQuietly(src);
}
if (isReusable(src)) {
//Only return the Source if it's reusable
log.debug("Returning Source for " + uri);
sessionSources.put(uri, src);
} else {
//Otherwise, try to close if possible and forget about it
XmlSourceUtil.closeQuietly(src);
}
}
/**
* Indicates whether a Source is reusable. A Source object is reusable if it's an
* {@link ImageSource} (containing an {@link ImageInputStream}) or a {@link DOMSource}.
* @param src the Source object
* @return true if the Source is reusable
*/
protected boolean isReusable(Source src) {
if (noSourceReuse) {
return false;
}
if (src instanceof ImageSource) {
ImageSource is = (ImageSource) src;
if (is.getImageInputStream() != null) {
return true;
}
}
if (src instanceof DOMSource) {
return true;
}
return false;
}
/**
* Implementations of this interface will be used as the mechanism for creating the
* {@link Source} that wraps resources. This interface allows clients to define their own
* implementations so that they have fine-grained control over how resources are acquired.
*/
public interface FallbackResolver {
/**
* The fallback mechanism used to create the source which takes in both the {@link Source}
* that the the regular mechanisms attempted to create and the URI that the user provided.
*
* @param source the source
* @param uri the URI provided by the user
* @return the source that the contingency mechanism has been acquired
*/
Source createSource(Source source, String uri);
}
/**
* An unrestricted resolver that has various contingency mechanisms that access the file-system.
* This is most suitable for use via the CLI or in environments where controlling I/O isn't a
* priority.
*/
public static final class UnrestrictedFallbackResolver implements FallbackResolver {
/** {@inheritDoc} */
public Source createSource(Source source, String uri) {
if (source == null) {
if (log.isDebugEnabled()) {
log.debug("URI could not be resolved: " + uri);
}
return null;
}
ImageSource imageSource = null;
String resolvedURI = source.getSystemId();
URL url;
try {
url = new URL(resolvedURI);
} catch (MalformedURLException e) {
url = null;
}
File f = /*FileUtils.*/toFile(url);
if (f != null) {
boolean directFileAccess = true;
assert (source instanceof StreamSource) || (source instanceof SAXSource);
InputStream in = XmlSourceUtil.getInputStream(source);
if (in == null) {
try {
in = new java.io.FileInputStream(f);
} catch (FileNotFoundException fnfe) {
log.error("Error while opening file."
+ " Could not load image from system identifier '"
+ source.getSystemId() + "' (" + fnfe.getMessage() + ")");
return null;
}
}
in = ImageUtil.decorateMarkSupported(in);
try {
if (ImageUtil.isGZIPCompressed(in)) {
//GZIPped stream are not seekable, so buffer/cache like other URLs
directFileAccess = false;
}
} catch (IOException ioe) {
log.error("Error while checking the InputStream for GZIP compression."
+ " Could not load image from system identifier '"
+ source.getSystemId() + "' (" + ioe.getMessage() + ")");
return null;
}
if (directFileAccess) {
//Close as the file is reopened in a more optimal way
IOUtils.closeQuietly(in);
try {
// We let the OS' file system cache do the caching for us
// --> lower Java memory consumption, probably no speed loss
final ImageInputStream newInputStream = ImageIO
.createImageInputStream(f);
if (newInputStream == null) {
log.error("Unable to create ImageInputStream for local file "
+ f
+ " from system identifier '"
+ source.getSystemId() + "'");
return null;
} else {
imageSource = new ImageSource(newInputStream,
resolvedURI, true);
}
} catch (IOException ioe) {
log.error("Unable to create ImageInputStream for local file"
+ " from system identifier '"
+ source.getSystemId() + "' (" + ioe.getMessage() + ")");
}
}
}
if (imageSource == null) {
if (XmlSourceUtil.hasReader(source) && !ImageUtil.hasInputStream(source)) {
//We don't handle Reader instances here so return the Source unchanged
return source;
}
// Got a valid source, obtain an InputStream from it
InputStream in = XmlSourceUtil.getInputStream(source);
if (in == null && url != null) {
try {
in = url.openStream();
} catch (Exception ex) {
log.error("Unable to obtain stream from system identifier '"
+ source.getSystemId() + "'");
}
}
if (in == null) {
log.error("The Source that was returned from URI resolution didn't contain"
+ " an InputStream for URI: " + uri);
return null;
}
return createImageSource(in, source);
}
return imageSource;
}
}
private static ImageSource createImageSource(InputStream in, Source source) {
try {
//Buffer and uncompress if necessary
return new ImageSource(createImageInputStream(ImageUtil.autoDecorateInputStream(in)),
source.getSystemId(), false);
} catch (IOException ioe) {
log.error("Unable to create ImageInputStream for InputStream"
+ " from system identifier '"
+ source.getSystemId() + "' (" + ioe.getMessage() + ")");
}
return null;
}
/**
* This fallback resolver is to be used in environments where controlling file access is of
* critical importance. It disallows any contingency mechanisms by which a {@link Source} object
* would be created.
*/
public static final class RestrictedFallbackResolver implements FallbackResolver {
/** {@inheritDoc} */
public Source createSource(Source source, String uri) {
if (source == null) {
if (log.isDebugEnabled()) {
log.debug("URI could not be resolved: " + uri);
}
return null;
}
if (ImageUtil.hasInputStream(source)) {
return createImageSource(XmlSourceUtil.getInputStream(source), source);
}
throw new UnsupportedOperationException("There are no contingency mechanisms for I/O.");
}
}
}