From 67a9815afa925606d13e13488e5539a57deb5bb2 Mon Sep 17 00:00:00 2001 From: Jeremias Maerki Date: Fri, 29 Jun 2007 12:46:14 +0000 Subject: [PATCH] Bugzilla #42278: Refactoring of color map cache and uri/fo resolution from FopFactory Submitted by: Adrian Cumiskey Changes in addition to the patch by jeremias: - Moved the color map cache to the util package so it doesn't clutter the API (apps) package. - Factored out the data URL resolution into its own URIResolver class which can now be used separately. - Added a utility class for generating RFC2397 data URLs. - Added a unit test for data URL handling. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@551874 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/fop/apps/FOURIResolver.java | 358 ++++++++++-------- src/java/org/apache/fop/apps/FOUserAgent.java | 15 +- src/java/org/apache/fop/apps/FopFactory.java | 129 ++----- .../org/apache/fop/util/ColorSpaceCache.java | 110 ++++++ .../org/apache/fop/util/DataURIResolver.java | 78 ++++ src/java/org/apache/fop/util/DataURLUtil.java | 67 ++++ .../apache/fop/util/WriterOutputStream.java | 91 +++++ .../org/apache/fop/UtilityCodeTestSuite.java | 4 + .../fop/util/DataURIResolverTestCase.java | 116 ++++++ 9 files changed, 715 insertions(+), 253 deletions(-) create mode 100644 src/java/org/apache/fop/util/ColorSpaceCache.java create mode 100644 src/java/org/apache/fop/util/DataURIResolver.java create mode 100644 src/java/org/apache/fop/util/DataURLUtil.java create mode 100644 src/java/org/apache/fop/util/WriterOutputStream.java create mode 100644 test/java/org/apache/fop/util/DataURIResolverTestCase.java diff --git a/src/java/org/apache/fop/apps/FOURIResolver.java b/src/java/org/apache/fop/apps/FOURIResolver.java index 13baeaa56..d68905e4c 100644 --- a/src/java/org/apache/fop/apps/FOURIResolver.java +++ b/src/java/org/apache/fop/apps/FOURIResolver.java @@ -19,7 +19,6 @@ package org.apache.fop.apps; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; @@ -30,28 +29,34 @@ import java.net.URLConnection; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; import javax.xml.transform.stream.StreamSource; // commons logging import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.fop.util.DataURIResolver; -// base64 support for "data" urls -import org.apache.xmlgraphics.util.io.Base64DecodeStream; import org.apache.xmlgraphics.util.io.Base64EncodeStream; /** - * Provides FOP specific URI resolution. - * This is the default URIResolver {@link FOUserAgent} will use unless overidden. + * Provides FOP specific URI resolution. This is the default URIResolver + * {@link FOUserAgent} will use unless overidden. + * * @see javax.xml.transform.URIResolver */ -public class FOURIResolver - implements javax.xml.transform.URIResolver { - +public class FOURIResolver implements javax.xml.transform.URIResolver { + // log private Log log = LogFactory.getLog("FOP"); - // true if exceptions are to be thrown if the URIs cannot be resolved. + /** URIResolver for RFC 2397 data URLs */ + private URIResolver dataURIResolver = new DataURIResolver(); + + /** A user settable URI Resolver */ + private URIResolver uriResolver = null; + + /** true if exceptions are to be thrown if the URIs cannot be resolved. */ private boolean throwExceptions = false; /** @@ -60,23 +65,28 @@ public class FOURIResolver public FOURIResolver() { this(false); } - + /** * Additional constructor - * @param throwExceptions true if exceptions are to be thrown if the URIs cannot be - * resolved. + * + * @param throwExceptions + * true if exceptions are to be thrown if the URIs cannot be + * resolved. */ public FOURIResolver(boolean throwExceptions) { this.throwExceptions = throwExceptions; } - + /** * Handles resolve exceptions appropriately. - * @param errorStr error string - * @param strict strict user config + * + * @param errorStr + * error string + * @param strict + * strict user config */ private void handleException(Exception e, String errorStr, boolean strict) - throws TransformerException { + throws TransformerException { if (strict) { throw new TransformerException(errorStr, e); } @@ -84,182 +94,220 @@ public class FOURIResolver } /** - * Called by the processor through {@link FOUserAgent} when it encounters an - * uri in an external-graphic element. - * (see also {@link javax.xml.transform.URIResolver#resolve(String, String)} - * This resolver will allow URLs without a scheme, i.e. it assumes 'file:' as - * the default scheme. It also allows relative URLs with scheme, - * e.g. file:../../abc.jpg which is not strictly RFC compliant as long as the - * scheme is the same as the scheme of the base URL. If the base URL is null + * Called by the processor through {@link FOUserAgent} when it encounters an + * uri in an external-graphic element. (see also + * {@link javax.xml.transform.URIResolver#resolve(String, String)} This + * resolver will allow URLs without a scheme, i.e. it assumes 'file:' as the + * default scheme. It also allows relative URLs with scheme, e.g. + * file:../../abc.jpg which is not strictly RFC compliant as long as the + * scheme is the same as the scheme of the base URL. If the base URL is null * a 'file:' URL referencing the current directory is used as the base URL. - * If the method is successful it will return a Source of type - * {@link javax.xml.transform.stream.StreamSource} with its SystemID set to + * If the method is successful it will return a Source of type + * {@link javax.xml.transform.stream.StreamSource} with its SystemID set to * the resolved URL used to open the underlying InputStream. * - * @param href An href attribute, which may be relative or absolute. - * @param base The base URI against which the first argument will be made - * absolute if the absolute URI is required. - * @return A {@link javax.xml.transform.Source} object, or null if the href - * cannot be resolved. - * @throws javax.xml.transform.TransformerException Never thrown by this implementation. + * @param href + * An href attribute, which may be relative or absolute. + * @param base + * The base URI against which the first argument will be made + * absolute if the absolute URI is required. + * @return A {@link javax.xml.transform.Source} object, or null if the href + * cannot be resolved. + * @throws javax.xml.transform.TransformerException + * Never thrown by this implementation. * @see javax.xml.transform.URIResolver#resolve(String, String) */ - public Source resolve(String href, String base) throws TransformerException { - // data URLs can be quite long so don't try to build a File (can lead to problems) - if (href.startsWith("data:")) { - return parseDataURI(href); + public Source resolve(String href, String base) throws TransformerException { + Source source = null; + + // data URLs can be quite long so evaluate early and don't try to build a File + // (can lead to problems) + source = dataURIResolver.resolve(href, base); + + // Custom uri resolution + if (source == null && uriResolver != null) { + source = uriResolver.resolve(href, base); } - URL absoluteURL = null; - File file = new File(href); - if (file.canRead() && file.isFile()) { - try { - absoluteURL = file.toURL(); - } catch (MalformedURLException mfue) { - handleException(mfue, - "Could not convert filename '" + href + "' to URL", throwExceptions); - } - } else { - // no base provided - if (base == null) { - // We don't have a valid file protocol based URL + // Fallback to default resolution mechanism + if (source == null) { + URL absoluteURL = null; + File file = new File(href); + if (file.canRead() && file.isFile()) { try { - absoluteURL = new URL(href); - } catch (MalformedURLException mue) { + absoluteURL = file.toURL(); + } catch (MalformedURLException mfue) { + handleException(mfue, "Could not convert filename '" + href + + "' to URL", throwExceptions); + } + } else { + // no base provided + if (base == null) { + // We don't have a valid file protocol based URL try { - // the above failed, we give it another go in case - // the href contains only a path then file: is assumed - absoluteURL = new URL("file:" + href); - } catch (MalformedURLException mfue) { - handleException(mfue, - "Error with URL '" + href + "'", throwExceptions); + absoluteURL = new URL(href); + } catch (MalformedURLException mue) { + try { + // the above failed, we give it another go in case + // the href contains only a path then file: is + // assumed + absoluteURL = new URL("file:" + href); + } catch (MalformedURLException mfue) { + handleException(mfue, "Error with URL '" + href + + "'", throwExceptions); + } } - } - // try and resolve from context of base - } else { - URL baseURL = null; - try { - baseURL = new URL(base); - } catch (MalformedURLException mfue) { - handleException(mfue, "Error with base URL '" + base + "'", throwExceptions); - } + // try and resolve from context of base + } else { + URL baseURL = null; + try { + baseURL = new URL(base); + } catch (MalformedURLException mfue) { + handleException(mfue, "Error with base URL '" + base + + "'", throwExceptions); + } - /* - * This piece of code is based on the following statement in - * RFC2396 section 5.2: - * - * 3) If the scheme component is defined, indicating that the - * reference starts with a scheme name, then the reference is - * interpreted as an absolute URI and we are done. Otherwise, - * the reference URI's scheme is inherited from the base URI's - * scheme component. - * - * Due to a loophole in prior specifications [RFC1630], some - * parsers allow the scheme name to be present in a relative URI - * if it is the same as the base URI scheme. Unfortunately, this - * can conflict with the correct parsing of non-hierarchical - * URI. For backwards compatibility, an implementation may work - * around such references by removing the scheme if it matches - * that of the base URI and the scheme is known to always use - * the syntax. - * - * The URL class does not implement this work around, so we do. - */ - String scheme = baseURL.getProtocol() + ":"; - if (href.startsWith(scheme)) { - href = href.substring(scheme.length()); - if ("file:".equals(scheme)) { - int colonPos = href.indexOf(':'); - int slashPos = href.indexOf('/'); - if (slashPos >= 0 && colonPos >= 0 && colonPos < slashPos) { - href = "/" + href; // Absolute file URL doesn't - // have a leading slash + /* + * This piece of code is based on the following statement in + * RFC2396 section 5.2: + * + * 3) If the scheme component is defined, indicating that + * the reference starts with a scheme name, then the + * reference is interpreted as an absolute URI and we are + * done. Otherwise, the reference URI's scheme is inherited + * from the base URI's scheme component. + * + * Due to a loophole in prior specifications [RFC1630], some + * parsers allow the scheme name to be present in a relative + * URI if it is the same as the base URI scheme. + * Unfortunately, this can conflict with the correct parsing + * of non-hierarchical URI. For backwards compatibility, an + * implementation may work around such references by + * removing the scheme if it matches that of the base URI + * and the scheme is known to always use the + * syntax. + * + * The URL class does not implement this work around, so we + * do. + */ + String scheme = baseURL.getProtocol() + ":"; + if (href.startsWith(scheme)) { + href = href.substring(scheme.length()); + if ("file:".equals(scheme)) { + int colonPos = href.indexOf(':'); + int slashPos = href.indexOf('/'); + if (slashPos >= 0 && colonPos >= 0 + && colonPos < slashPos) { + href = "/" + href; // Absolute file URL doesn't + // have a leading slash + } } } + try { + absoluteURL = new URL(baseURL, href); + } catch (MalformedURLException mfue) { + handleException(mfue, "Error with URL; base '" + base + + "' " + "href '" + href + "'", throwExceptions); + } } + } + + if (absoluteURL != null) { + String effURL = absoluteURL.toExternalForm(); try { - absoluteURL = new URL(baseURL, href); - } catch (MalformedURLException mfue) { - handleException(mfue, - "Error with URL; base '" + base + "' " + "href '" + href + "'", - throwExceptions); + URLConnection connection = absoluteURL.openConnection(); + connection.setAllowUserInteraction(false); + connection.setDoInput(true); + updateURLConnection(connection, href); + connection.connect(); + return new StreamSource(connection.getInputStream(), effURL); + } catch (FileNotFoundException fnfe) { + // Note: This is on "debug" level since the caller is + // supposed to handle this + log.debug("File not found: " + effURL); + } catch (java.io.IOException ioe) { + log.error("Error with opening URL '" + effURL + "': " + + ioe.getMessage()); } } } - - if (absoluteURL != null) { - String effURL = absoluteURL.toExternalForm(); - try { - URLConnection connection = absoluteURL.openConnection(); - connection.setAllowUserInteraction(false); - connection.setDoInput(true); - updateURLConnection(connection, href); - connection.connect(); - return new StreamSource(connection.getInputStream(), effURL); - } catch (FileNotFoundException fnfe) { - //Note: This is on "debug" level since the caller is supposed to handle this - log.debug("File not found: " + effURL); - } catch (java.io.IOException ioe) { - log.error("Error with opening URL '" + effURL + "': " + ioe.getMessage()); - } - } - return null; + return source; } /** - * This method allows you to set special values on a URLConnection just before the connect() - * method is called. Subclass FOURIResolver and override this method to do things like - * adding the user name and password for HTTP basic authentication. - * @param connection the URLConnection instance - * @param href the original URI + * This method allows you to set special values on a URLConnection just + * before the connect() method is called. Subclass FOURIResolver and + * override this method to do things like adding the user name and password + * for HTTP basic authentication. + * + * @param connection + * the URLConnection instance + * @param href + * the original URI */ protected void updateURLConnection(URLConnection connection, String href) { - //nop + // nop } - + /** - * This is a convenience method for users who want to override updateURLConnection for - * HTTP basic authentication. Simply call it using the right username and password. - * @param connection the URLConnection to set up for HTTP basic authentication - * @param username the username - * @param password the password + * This is a convenience method for users who want to override + * updateURLConnection for HTTP basic authentication. Simply call it using + * the right username and password. + * + * @param connection + * the URLConnection to set up for HTTP basic authentication + * @param username + * the username + * @param password + * the password */ - protected void applyHttpBasicAuthentication(URLConnection connection, + protected void applyHttpBasicAuthentication(URLConnection connection, String username, String password) { String combined = username + ":" + password; try { - ByteArrayOutputStream baout = new ByteArrayOutputStream(combined.length() * 2); + ByteArrayOutputStream baout = new ByteArrayOutputStream(combined + .length() * 2); Base64EncodeStream base64 = new Base64EncodeStream(baout); - //TODO Not sure what charset/encoding can be used with basic authentication + // TODO Not sure what charset/encoding can be used with basic + // authentication base64.write(combined.getBytes("UTF-8")); base64.close(); - connection.setRequestProperty("Authorization", - "Basic " + new String(baout.toByteArray(), "UTF-8")); + connection.setRequestProperty("Authorization", "Basic " + + new String(baout.toByteArray(), "UTF-8")); } catch (IOException e) { - //won't happen. We're operating in-memory. - throw new RuntimeException("Error during base64 encodation of username/password"); + // won't happen. We're operating in-memory. + throw new RuntimeException( + "Error during base64 encodation of username/password"); } } - + /** - * Parses inline data URIs as generated by MS Word's XML export and FO stylesheet. - * @see RFC 2397 + * Sets the custom URI Resolver. It is used for resolving factory-level URIs like + * hyphenation patterns and as backup for URI resolution performed during a + * rendering run. + * + * @param resolver + * the new URI resolver */ - private Source parseDataURI(String href) { - int commaPos = href.indexOf(','); - // header is of the form data:[][;base64] - String header = href.substring(0, commaPos); - String data = href.substring(commaPos + 1); - if (header.endsWith(";base64")) { - byte[] bytes = data.getBytes(); - ByteArrayInputStream encodedStream = new ByteArrayInputStream(bytes); - Base64DecodeStream decodedStream = new Base64DecodeStream(encodedStream); - return new StreamSource(decodedStream); - } else { - //Note that this is not quite the full story here. But since we are only interested - //in base64-encoded binary data, the next line will probably never be called. - return new StreamSource(new java.io.StringReader(data)); - } + public void setCustomURIResolver(URIResolver resolver) { + this.uriResolver = resolver; + } + + /** + * Returns the custom URI Resolver. + * + * @return the URI Resolver or null, if none is set + */ + public URIResolver getCustomURIResolver() { + return this.uriResolver; + } + + /** + * @param throwExceptions + * Whether or not to throw exceptions on resolution error + */ + public void setThrowExceptions(boolean throwExceptions) { + this.throwExceptions = throwExceptions; } } diff --git a/src/java/org/apache/fop/apps/FOUserAgent.java b/src/java/org/apache/fop/apps/FOUserAgent.java index 24df2f75b..33106ee12 100644 --- a/src/java/org/apache/fop/apps/FOUserAgent.java +++ b/src/java/org/apache/fop/apps/FOUserAgent.java @@ -361,26 +361,27 @@ public class FOUserAgent { * Attempts to resolve the given URI. * Will use the configured resolver and if not successful fall back * to the default resolver. - * @param uri URI to access + * @param href URI to access * @param base the base URI to resolve against * @return A {@link javax.xml.transform.Source} object, or null if the URI * cannot be resolved. * @see org.apache.fop.apps.FOURIResolver */ - public Source resolveURI(String uri, String base) { + public Source resolveURI(String href, String base) { Source source = null; - //RFC 2397 data URLs don't need to be resolved, just decode them. - boolean bypassURIResolution = uri.startsWith("data:"); + //RFC 2397 data URLs don't need to be resolved, just decode them through FOP's default + //URIResolver. + boolean bypassURIResolution = href.startsWith("data:"); if (!bypassURIResolution && uriResolver != null) { try { - source = uriResolver.resolve(uri, base); + source = uriResolver.resolve(href, base); } catch (TransformerException te) { - log.error("Attempt to resolve URI '" + uri + "' failed: ", te); + log.error("Attempt to resolve URI '" + href + "' failed: ", te); } } if (source == null) { // URI Resolver not configured or returned null, use default resolver from the factory - source = getFactory().resolveURI(uri, base); + source = getFactory().resolveURI(href, base); } return source; } diff --git a/src/java/org/apache/fop/apps/FopFactory.java b/src/java/org/apache/fop/apps/FopFactory.java index 1e7ab2a62..99af1671a 100644 --- a/src/java/org/apache/fop/apps/FopFactory.java +++ b/src/java/org/apache/fop/apps/FopFactory.java @@ -15,13 +15,11 @@ * limitations under the License. */ -/* $Id$ */ +/* $Id: $ */ package org.apache.fop.apps; import java.awt.color.ColorSpace; -import java.awt.color.ICC_ColorSpace; -import java.awt.color.ICC_Profile; import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -29,13 +27,11 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.Collections; -import java.util.Map; import java.util.Set; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.URIResolver; -import javax.xml.transform.stream.StreamSource; import org.xml.sax.SAXException; @@ -52,6 +48,7 @@ import org.apache.fop.image.ImageFactory; import org.apache.fop.layoutmgr.LayoutManagerMaker; import org.apache.fop.render.RendererFactory; import org.apache.fop.render.XMLHandlerRegistry; +import org.apache.fop.util.ColorSpaceCache; import org.apache.fop.util.ContentHandlerFactoryRegistry; /** @@ -66,10 +63,10 @@ public class FopFactory { private static Log log = LogFactory.getLog(FopFactory.class); /** Factory for Renderers and FOEventHandlers */ - private RendererFactory rendererFactory = new RendererFactory(); + private RendererFactory rendererFactory; /** Registry for XML handlers */ - private XMLHandlerRegistry xmlHandlers = new XMLHandlerRegistry(); + private XMLHandlerRegistry xmlHandlers; /** The registry for ElementMapping instances */ private ElementMappingRegistry elementMappingRegistry; @@ -78,17 +75,13 @@ public class FopFactory { private ContentHandlerFactoryRegistry contentHandlerFactoryRegistry = new ContentHandlerFactoryRegistry(); - /** Our default resolver if none is set */ - private URIResolver foURIResolver = null; - - /** A user settable URI Resolver */ - private URIResolver uriResolver = null; - /** The resolver for user-supplied hyphenation patterns */ - private HyphenationTreeResolver hyphResolver; + private HyphenationTreeResolver hyphResolver = null; + + private ColorSpaceCache colorSpaceCache = null; /** Image factory for creating fop image objects */ - private ImageFactory imageFactory = new ImageFactory(); + private ImageFactory imageFactory; /** Configuration layer used to configure fop */ private FopFactoryConfigurator config = null; @@ -145,11 +138,10 @@ public class FopFactory { /** Optional overriding LayoutManagerMaker */ private LayoutManagerMaker lmMakerOverride = null; - private Set ignoredNamespaces = new java.util.HashSet(); + private Set ignoredNamespaces; + + private FOURIResolver foURIResolver; - /** Map with cached ICC based ColorSpace objects. */ - private Map colorSpaceMap = null; - /** * Main constructor. */ @@ -157,8 +149,11 @@ public class FopFactory { this.config = new FopFactoryConfigurator(this); this.elementMappingRegistry = new ElementMappingRegistry(this); this.foURIResolver = new FOURIResolver(validateUserConfigStrictly()); - // Use a synchronized Map - I am not really sure this is needed, but better safe than sorry. - this.colorSpaceMap = Collections.synchronizedMap(new java.util.HashMap()); + this.colorSpaceCache = new ColorSpaceCache(foURIResolver); + this.imageFactory = new ImageFactory(); + this.rendererFactory = new RendererFactory(); + this.xmlHandlers = new XMLHandlerRegistry(); + this.ignoredNamespaces = new java.util.HashSet(); setUseCache(FopFactoryConfigurator.DEFAULT_USE_CACHE); } @@ -397,11 +392,12 @@ public class FopFactory { * */ public void setHyphenBaseURL(final String hyphenBase) throws MalformedURLException { if (hyphenBase != null) { - this.hyphResolver = new HyphenationTreeResolver() { + setHyphenationTreeResolver( + new HyphenationTreeResolver() { public Source resolve(String href) { return resolveURI(href, hyphenBase); } - }; + }); } this.hyphenBase = checkBaseURL(hyphenBase); } @@ -411,8 +407,8 @@ public class FopFactory { * patterns and as backup for URI resolution performed during a rendering run. * @param resolver the new URI resolver */ - public void setURIResolver(URIResolver resolver) { - this.uriResolver = resolver; + public void setURIResolver(URIResolver uriResolver) { + foURIResolver.setCustomURIResolver(uriResolver); } /** @@ -420,7 +416,7 @@ public class FopFactory { * @return the URI Resolver */ public URIResolver getURIResolver() { - return this.uriResolver; + return foURIResolver; } /** @return the HyphenationTreeResolver for resolving user-supplied hyphenation patterns. */ @@ -428,6 +424,14 @@ public class FopFactory { return this.hyphResolver; } + /** + * sets the HyphenationTreeResolver + * @param hyphResolver + */ + public void setHyphenationTreeResolver(HyphenationTreeResolver hyphResolver) { + this.hyphResolver = hyphResolver; + } + /** * Activates strict XSL content model validation for FOP * Default is false (FOP will continue processing where it can) @@ -669,6 +673,7 @@ public class FopFactory { */ public void setStrictUserConfigValidation(boolean strictUserConfigValidation) { this.strictUserConfigValidation = strictUserConfigValidation; + this.foURIResolver.setThrowExceptions(strictUserConfigValidation); } /** @@ -708,39 +713,22 @@ public class FopFactory { return this.fontCache; } - //------------------------------------------- URI resolution - /** * Attempts to resolve the given URI. * Will use the configured resolver and if not successful fall back * to the default resolver. - * @param uri URI to access + * @param href URI to access * @param baseUri the base URI to resolve against * @return A {@link javax.xml.transform.Source} object, or null if the URI * cannot be resolved. * @see org.apache.fop.apps.FOURIResolver */ - public Source resolveURI(String uri, String baseUri) { + public Source resolveURI(String href, String baseUri) { Source source = null; - //RFC 2397 data URLs don't need to be resolved, just decode them. - boolean bypassURIResolution = uri.startsWith("data:"); - if (!bypassURIResolution && uriResolver != null) { - try { - source = uriResolver.resolve(uri, baseUri); - } catch (TransformerException te) { - log.error("Attempt to resolve URI '" + uri + "' failed: ", te); - if (validateUserConfigStrictly()) { - return null; - } - } - } - if (source == null) { - // URI Resolver not configured or returned null, use default resolver - try { - source = foURIResolver.resolve(uri, baseUri); - } catch (TransformerException te) { - log.error("Attempt to resolve URI '" + uri + "' failed: ", te); - } + try { + source = foURIResolver.resolve(href, baseUri); + } catch (TransformerException e) { + log.error("Attempt to resolve URI '" + href + "' failed: ", e); } return source; } @@ -759,47 +747,6 @@ public class FopFactory { * @return ICC ColorSpace object or null if ColorSpace could not be created */ public ColorSpace getColorSpace(String baseUri, String iccProfileSrc) { - ColorSpace colorSpace = null; - if (!this.colorSpaceMap.containsKey(baseUri + iccProfileSrc)) { - try { - ICC_Profile iccProfile = null; - // First attempt to use the FOP URI resolver to locate the ICC - // profile - Source src = this.resolveURI(iccProfileSrc, baseUri); - if (src != null && src instanceof StreamSource) { - // FOP URI resolver found ICC profile - create ICC profile - // from the Source - iccProfile = ICC_Profile.getInstance(((StreamSource) src) - .getInputStream()); - } else { - // TODO - Would it make sense to fall back on VM ICC - // resolution - // Problem is the cache might be more difficult to maintain - // - // FOP URI resolver did not find ICC profile - perhaps the - // Java VM can find it? - // iccProfile = ICC_Profile.getInstance(iccProfileSrc); - } - if (iccProfile != null) { - colorSpace = new ICC_ColorSpace(iccProfile); - } - } catch (IOException e) { - // Ignore exception - will be logged a bit further down - // (colorSpace == null case) - } - - if (colorSpace != null) { - // Put in cache (not when VM resolved it as we can't control - this.colorSpaceMap.put(baseUri + iccProfileSrc, colorSpace); - } else { - // TODO To avoid an excessive amount of warnings perhaps - // register a null ColorMap in the colorSpaceMap - log.warn("Color profile '" + iccProfileSrc + "' not found."); - } - } else { - colorSpace = (ColorSpace) this.colorSpaceMap.get(baseUri - + iccProfileSrc); - } - return colorSpace; + return colorSpaceCache.get(baseUri, iccProfileSrc); } } diff --git a/src/java/org/apache/fop/util/ColorSpaceCache.java b/src/java/org/apache/fop/util/ColorSpaceCache.java new file mode 100644 index 000000000..92dcf8d55 --- /dev/null +++ b/src/java/org/apache/fop/util/ColorSpaceCache.java @@ -0,0 +1,110 @@ +/* + * 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.fop.util; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.util.Collections; +import java.util.Map; + +import javax.xml.transform.Source; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Map with cached ICC based ColorSpace objects. + */ +public class ColorSpaceCache { + /** logger instance */ + private static Log log = LogFactory.getLog(ColorSpaceCache.class); + + private URIResolver resolver; + private Map colorSpaceMap = Collections.synchronizedMap(new java.util.HashMap()); + + /** + * Default constructor + * @param resolver uri resolver + */ + public ColorSpaceCache(URIResolver resolver) { + this.resolver = resolver; + } + + /** + * Create (if needed) and return an ICC ColorSpace instance. + * + * The ICC profile source is taken from the src attribute of the color-profile FO element. + * If the ICC ColorSpace is not yet in the cache a new one is created and stored in the cache. + * + * The FOP URI resolver is used to try and locate the ICC file. + * If that fails null is returned. + * + * @param base a base URI to resolve relative URIs + * @param iccProfileSrc ICC Profile source to return a ColorSpace for + * @return ICC ColorSpace object or null if ColorSpace could not be created + */ + public ColorSpace get(String base, String iccProfileSrc) { + ColorSpace colorSpace = null; + if (!colorSpaceMap.containsKey(base + iccProfileSrc)) { + try { + ICC_Profile iccProfile = null; + // First attempt to use the FOP URI resolver to locate the ICC + // profile + Source src = resolver.resolve(iccProfileSrc, base); + if (src != null && src instanceof StreamSource) { + // FOP URI resolver found ICC profile - create ICC profile + // from the Source + iccProfile = ICC_Profile.getInstance(((StreamSource) src) + .getInputStream()); + } else { + // TODO - Would it make sense to fall back on VM ICC + // resolution + // Problem is the cache might be more difficult to maintain + // + // FOP URI resolver did not find ICC profile - perhaps the + // Java VM can find it? + // iccProfile = ICC_Profile.getInstance(iccProfileSrc); + } + if (iccProfile != null) { + colorSpace = new ICC_ColorSpace(iccProfile); + } + } catch (Exception e) { + // Ignore exception - will be logged a bit further down + // (colorSpace == null case) + } + + if (colorSpace != null) { + // Put in cache (not when VM resolved it as we can't control + colorSpaceMap.put(base + iccProfileSrc, colorSpace); + } else { + // TODO To avoid an excessive amount of warnings perhaps + // register a null ColorMap in the colorSpaceMap + log.warn("Color profile '" + iccProfileSrc + "' not found."); + } + } else { + colorSpace = (ColorSpace)colorSpaceMap.get(base + + iccProfileSrc); + } + return colorSpace; + } +} diff --git a/src/java/org/apache/fop/util/DataURIResolver.java b/src/java/org/apache/fop/util/DataURIResolver.java new file mode 100644 index 000000000..4ae4be156 --- /dev/null +++ b/src/java/org/apache/fop/util/DataURIResolver.java @@ -0,0 +1,78 @@ +/* + * 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.fop.util; + +import java.io.ByteArrayInputStream; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +// base64 support for "data" urls +import org.apache.xmlgraphics.util.io.Base64DecodeStream; + +/** + * Resolves data URLs (described in RFC 2397) returning its data as a StreamSource. + * + * @see javax.xml.transform.URIResolver + * @see RFC 2397 + */ +public class DataURIResolver implements URIResolver { + + /** + * @see javax.xml.transform.URIResolver#resolve(java.lang.String, java.lang.String) + */ + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("data:")) { + return parseDataURI(href); + } else { + return null; + } + } + + /** + * Parses inline data URIs as generated by MS Word's XML export and FO + * stylesheet. + * + * @see RFC 2397 + */ + private Source parseDataURI(String href) { + int commaPos = href.indexOf(','); + // header is of the form data:[][;base64] + String header = href.substring(0, commaPos); + String data = href.substring(commaPos + 1); + if (header.endsWith(";base64")) { + byte[] bytes = data.getBytes(); + ByteArrayInputStream encodedStream = new ByteArrayInputStream(bytes); + Base64DecodeStream decodedStream = new Base64DecodeStream( + encodedStream); + return new StreamSource(decodedStream); + } else { + // Note that this is not quite the full story here. But since we are + // only interested + // in base64-encoded binary data, the next line will probably never + // be called. + //TODO Handle un-escaping of special URL chars like %20 + return new StreamSource(new java.io.StringReader(data)); + } + } + +} diff --git a/src/java/org/apache/fop/util/DataURLUtil.java b/src/java/org/apache/fop/util/DataURLUtil.java new file mode 100644 index 000000000..03236dd45 --- /dev/null +++ b/src/java/org/apache/fop/util/DataURLUtil.java @@ -0,0 +1,67 @@ +/* + * 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.fop.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.Writer; + +import org.apache.commons.io.IOUtils; +import org.apache.xmlgraphics.util.io.Base64EncodeStream; + +/** + * Utility classes for generating RFC 2397 data URLs. + */ +public class DataURLUtil { + + /** + * Creates a new data URL and returns it as a String. + * @param in the InputStream to read the data from + * @param mediatype the MIME type of the content, or null + * @return the newly created data URL + * @throws IOException if an I/O error occurs + */ + public static String createDataURL(InputStream in, String mediatype) throws IOException { + StringWriter writer = new StringWriter(); + writeDataURL(in, mediatype, writer); + return writer.toString(); + } + + /** + * Generates a data URL and writes it to a Writer. + * @param in the InputStream to read the data from + * @param mediatype the MIME type of the content, or null + * @param writer the Writer to write to + * @throws IOException if an I/O error occurs + */ + public static void writeDataURL(InputStream in, String mediatype, Writer writer) + throws IOException { + writer.write("data:"); + if (mediatype != null) { + writer.write(mediatype); + } + writer.write(";base64,"); + Base64EncodeStream out = new Base64EncodeStream( + new WriterOutputStream(writer, "US-ASCII")); + IOUtils.copy(in, out); + out.flush(); + } +} diff --git a/src/java/org/apache/fop/util/WriterOutputStream.java b/src/java/org/apache/fop/util/WriterOutputStream.java new file mode 100644 index 000000000..d1908996a --- /dev/null +++ b/src/java/org/apache/fop/util/WriterOutputStream.java @@ -0,0 +1,91 @@ +/* + * 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.fop.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +/** + * An OutputStream wrapper for a Writer. + */ +public class WriterOutputStream extends OutputStream { + + private Writer writer; + private String encoding; + + /** + * Creates a new WriterOutputStream. + * @param writer the Writer to write to + */ + public WriterOutputStream(Writer writer) { + this(writer, null); + } + + /** + * Creates a new WriterOutputStream. + * @param writer the Writer to write to + * @param encoding the encoding to use, or null if the default encoding should be used + */ + public WriterOutputStream(Writer writer, String encoding) { + this.writer = writer; + this.encoding = encoding; + } + + /** + * @see java.io.OutputStream#close() + */ + public void close() throws IOException { + writer.close(); + } + + /** + * @see java.io.OutputStream#flush() + */ + public void flush() throws IOException { + writer.flush(); + } + + /** + * @see java.io.OutputStream#write(byte[], int, int) + */ + public void write(byte[] buf, int offset, int length) throws IOException { + if (encoding != null) { + writer.write(new String(buf, offset, length, encoding)); + } else { + writer.write(new String(buf, offset, length)); + } + } + + /** + * @see java.io.OutputStream#write(byte[]) + */ + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + + /** + * @see java.io.OutputStream#write(int) + */ + public void write(int b) throws IOException { + write(new byte[] {(byte)b}); + } + +} diff --git a/test/java/org/apache/fop/UtilityCodeTestSuite.java b/test/java/org/apache/fop/UtilityCodeTestSuite.java index f84390518..679e16ce7 100644 --- a/test/java/org/apache/fop/UtilityCodeTestSuite.java +++ b/test/java/org/apache/fop/UtilityCodeTestSuite.java @@ -21,6 +21,8 @@ package org.apache.fop; import org.apache.fop.traits.BorderPropsTestCase; import org.apache.fop.traits.TraitColorTestCase; +import org.apache.fop.util.DataURIResolverTestCase; +import org.apache.fop.util.ElementListUtilsTestCase; import org.apache.fop.util.PDFNumberTestCase; import org.apache.fop.util.UnitConvTestCase; @@ -44,6 +46,8 @@ public class UtilityCodeTestSuite { suite.addTest(new TestSuite(UnitConvTestCase.class)); suite.addTest(new TestSuite(TraitColorTestCase.class)); suite.addTest(new TestSuite(BorderPropsTestCase.class)); + suite.addTest(new TestSuite(ElementListUtilsTestCase.class)); + suite.addTest(new TestSuite(DataURIResolverTestCase.class)); //$JUnit-END$ return suite; } diff --git a/test/java/org/apache/fop/util/DataURIResolverTestCase.java b/test/java/org/apache/fop/util/DataURIResolverTestCase.java new file mode 100644 index 000000000..133d4fcd1 --- /dev/null +++ b/test/java/org/apache/fop/util/DataURIResolverTestCase.java @@ -0,0 +1,116 @@ +/* + * 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.fop.util; + +import java.io.ByteArrayInputStream; + +import javax.xml.transform.Source; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.io.IOUtils; + +import junit.framework.TestCase; + +/** + * Test case for the RFC 2397 data URL/URI resolver. + */ +public class DataURIResolverTestCase extends TestCase { + + private static final byte[] TESTDATA = new byte[] {0, 1, 2, 3, 4, 5}; + + /** + * Tests DataURLUtil. + * @throws Exception if an error occurs + */ + public void testRFC2397Generator() throws Exception { + String url = DataURLUtil.createDataURL(new ByteArrayInputStream(TESTDATA), null); + assertEquals("Generated data URL is wrong", "data:;base64,AAECAwQF", url); + + url = DataURLUtil.createDataURL(new ByteArrayInputStream(TESTDATA), "application/pdf"); + assertEquals("Generated data URL is wrong", "data:application/pdf;base64,AAECAwQF", url); + } + + /** + * Test the URIResolver contract if the protocol doesn't match. Resolver must return null + * in this case. + * @throws Exception if an error occurs + */ + public void testNonMatchingContract() throws Exception { + URIResolver resolver = new DataURIResolver(); + Source src; + + src = resolver.resolve("http://xmlgraphics.apache.org/fop/index.html", null); + assertNull(src); + + src = resolver.resolve("index.html", "http://xmlgraphics.apache.org/fop/"); + assertNull(src); + } + + private static boolean byteCmp(byte[] src, int srcOffset, byte[] cmp) { + for (int i = 0, c = cmp.length; i < c; i++) { + if (src[srcOffset + i] != cmp[i]) { + return false; + } + } + return true; + } + + /** + * Test the DataURIResolver with correct values. + * @throws Exception if an error occurs + */ + public void testDataURLHandling() throws Exception { + URIResolver resolver = new DataURIResolver(); + Source src; + + src = resolver.resolve("data:;base64,AAECAwQF", null); + assertNotNull(src); + StreamSource streamSource = (StreamSource)src; + byte[] data = IOUtils.toByteArray(streamSource.getInputStream()); + assertTrue("Decoded data doesn't match the test data", byteCmp(TESTDATA, 0, data)); + + src = resolver.resolve( + "data:application/octet-stream;interpreter=fop;base64,AAECAwQF", null); + assertNotNull(src); + streamSource = (StreamSource)src; + assertNotNull(streamSource.getInputStream()); + assertNull(streamSource.getReader()); + data = IOUtils.toByteArray(streamSource.getInputStream()); + assertTrue("Decoded data doesn't match the test data", byteCmp(TESTDATA, 0, data)); + + src = resolver.resolve("data:,FOP", null); + assertNotNull(src); + streamSource = (StreamSource)src; + assertNull(streamSource.getInputStream()); + assertNotNull(streamSource.getReader()); + String text = IOUtils.toString(streamSource.getReader()); + assertEquals("FOP", text); + + /* TODO Un-escaping of special URL chars like %20 hasn't been implemented, yet. + src = resolver.resolve("data:,A%20brief%20note", null); + assertNotNull(src); + streamSource = (StreamSource)src; + text = IOUtils.toString(streamSource.getReader()); + assertEquals("A brief note", text); + */ + } + +} -- 2.39.5