You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FOURIResolver.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.apps;
  19. import java.io.ByteArrayInputStream;
  20. import java.io.ByteArrayOutputStream;
  21. import java.io.File;
  22. import java.io.FileNotFoundException;
  23. import java.io.IOException;
  24. import java.net.MalformedURLException;
  25. import java.net.URL;
  26. import java.net.URLConnection;
  27. import javax.xml.transform.Source;
  28. import javax.xml.transform.TransformerException;
  29. import javax.xml.transform.stream.StreamSource;
  30. // commons logging
  31. import org.apache.commons.logging.Log;
  32. import org.apache.commons.logging.LogFactory;
  33. // base64 support for "data" urls
  34. import org.apache.xmlgraphics.util.io.Base64DecodeStream;
  35. import org.apache.xmlgraphics.util.io.Base64EncodeStream;
  36. /**
  37. * Provides FOP specific URI resolution.
  38. * This is the default URIResolver {@link FOUserAgent} will use unless overidden.
  39. * @see javax.xml.transform.URIResolver
  40. */
  41. public class FOURIResolver
  42. implements javax.xml.transform.URIResolver {
  43. // log
  44. private Log log = LogFactory.getLog("FOP");
  45. // true if exceptions are to be thrown if the URIs cannot be resolved.
  46. private boolean throwExceptions = false;
  47. /**
  48. * Default constructor
  49. */
  50. public FOURIResolver() {
  51. this(false);
  52. }
  53. /**
  54. * Additional constructor
  55. * @param throwExceptions true if exceptions are to be thrown if the URIs cannot be
  56. * resolved.
  57. */
  58. public FOURIResolver(boolean throwExceptions) {
  59. this.throwExceptions = throwExceptions;
  60. }
  61. /**
  62. * Handles resolve exceptions appropriately.
  63. * @param errorStr error string
  64. * @param strict strict user config
  65. */
  66. private void handleException(Exception e, String errorStr, boolean strict)
  67. throws TransformerException {
  68. if (strict) {
  69. throw new TransformerException(errorStr, e);
  70. }
  71. log.error(e.getMessage());
  72. }
  73. /**
  74. * Called by the processor through {@link FOUserAgent} when it encounters an
  75. * uri in an external-graphic element.
  76. * (see also {@link javax.xml.transform.URIResolver#resolve(String, String)}
  77. * This resolver will allow URLs without a scheme, i.e. it assumes 'file:' as
  78. * the default scheme. It also allows relative URLs with scheme,
  79. * e.g. file:../../abc.jpg which is not strictly RFC compliant as long as the
  80. * scheme is the same as the scheme of the base URL. If the base URL is null
  81. * a 'file:' URL referencing the current directory is used as the base URL.
  82. * If the method is successful it will return a Source of type
  83. * {@link javax.xml.transform.stream.StreamSource} with its SystemID set to
  84. * the resolved URL used to open the underlying InputStream.
  85. *
  86. * @param href An href attribute, which may be relative or absolute.
  87. * @param base The base URI against which the first argument will be made
  88. * absolute if the absolute URI is required.
  89. * @return A {@link javax.xml.transform.Source} object, or null if the href
  90. * cannot be resolved.
  91. * @throws javax.xml.transform.TransformerException Never thrown by this implementation.
  92. * @see javax.xml.transform.URIResolver#resolve(String, String)
  93. */
  94. public Source resolve(String href, String base) throws TransformerException {
  95. // data URLs can be quite long so don't try to build a File (can lead to problems)
  96. if (href.startsWith("data:")) {
  97. return parseDataURI(href);
  98. }
  99. URL absoluteURL = null;
  100. File file = new File(href);
  101. if (file.canRead() && file.isFile()) {
  102. try {
  103. absoluteURL = file.toURL();
  104. } catch (MalformedURLException mfue) {
  105. handleException(mfue,
  106. "Could not convert filename '" + href + "' to URL", throwExceptions);
  107. }
  108. } else {
  109. // no base provided
  110. if (base == null) {
  111. // We don't have a valid file protocol based URL
  112. try {
  113. absoluteURL = new URL(href);
  114. } catch (MalformedURLException mue) {
  115. try {
  116. // the above failed, we give it another go in case
  117. // the href contains only a path then file: is assumed
  118. absoluteURL = new URL("file:" + href);
  119. } catch (MalformedURLException mfue) {
  120. handleException(mfue,
  121. "Error with URL '" + href + "'", throwExceptions);
  122. }
  123. }
  124. // try and resolve from context of base
  125. } else {
  126. URL baseURL = null;
  127. try {
  128. baseURL = new URL(base);
  129. } catch (MalformedURLException mfue) {
  130. handleException(mfue, "Error with base URL '" + base + "'", throwExceptions);
  131. }
  132. /*
  133. * This piece of code is based on the following statement in
  134. * RFC2396 section 5.2:
  135. *
  136. * 3) If the scheme component is defined, indicating that the
  137. * reference starts with a scheme name, then the reference is
  138. * interpreted as an absolute URI and we are done. Otherwise,
  139. * the reference URI's scheme is inherited from the base URI's
  140. * scheme component.
  141. *
  142. * Due to a loophole in prior specifications [RFC1630], some
  143. * parsers allow the scheme name to be present in a relative URI
  144. * if it is the same as the base URI scheme. Unfortunately, this
  145. * can conflict with the correct parsing of non-hierarchical
  146. * URI. For backwards compatibility, an implementation may work
  147. * around such references by removing the scheme if it matches
  148. * that of the base URI and the scheme is known to always use
  149. * the <hier_part> syntax.
  150. *
  151. * The URL class does not implement this work around, so we do.
  152. */
  153. String scheme = baseURL.getProtocol() + ":";
  154. if (href.startsWith(scheme)) {
  155. href = href.substring(scheme.length());
  156. if ("file:".equals(scheme)) {
  157. int colonPos = href.indexOf(':');
  158. int slashPos = href.indexOf('/');
  159. if (slashPos >= 0 && colonPos >= 0 && colonPos < slashPos) {
  160. href = "/" + href; // Absolute file URL doesn't
  161. // have a leading slash
  162. }
  163. }
  164. }
  165. try {
  166. absoluteURL = new URL(baseURL, href);
  167. } catch (MalformedURLException mfue) {
  168. handleException(mfue,
  169. "Error with URL; base '" + base + "' " + "href '" + href + "'",
  170. throwExceptions);
  171. }
  172. }
  173. }
  174. if (absoluteURL != null) {
  175. String effURL = absoluteURL.toExternalForm();
  176. try {
  177. URLConnection connection = absoluteURL.openConnection();
  178. connection.setAllowUserInteraction(false);
  179. connection.setDoInput(true);
  180. updateURLConnection(connection, href);
  181. connection.connect();
  182. return new StreamSource(connection.getInputStream(), effURL);
  183. } catch (FileNotFoundException fnfe) {
  184. //Note: This is on "debug" level since the caller is supposed to handle this
  185. log.debug("File not found: " + effURL);
  186. } catch (java.io.IOException ioe) {
  187. log.error("Error with opening URL '" + effURL + "': " + ioe.getMessage());
  188. }
  189. }
  190. return null;
  191. }
  192. /**
  193. * This method allows you to set special values on a URLConnection just before the connect()
  194. * method is called. Subclass FOURIResolver and override this method to do things like
  195. * adding the user name and password for HTTP basic authentication.
  196. * @param connection the URLConnection instance
  197. * @param href the original URI
  198. */
  199. protected void updateURLConnection(URLConnection connection, String href) {
  200. //nop
  201. }
  202. /**
  203. * This is a convenience method for users who want to override updateURLConnection for
  204. * HTTP basic authentication. Simply call it using the right username and password.
  205. * @param connection the URLConnection to set up for HTTP basic authentication
  206. * @param username the username
  207. * @param password the password
  208. */
  209. protected void applyHttpBasicAuthentication(URLConnection connection,
  210. String username, String password) {
  211. String combined = username + ":" + password;
  212. try {
  213. ByteArrayOutputStream baout = new ByteArrayOutputStream(combined.length() * 2);
  214. Base64EncodeStream base64 = new Base64EncodeStream(baout);
  215. //TODO Not sure what charset/encoding can be used with basic authentication
  216. base64.write(combined.getBytes("UTF-8"));
  217. base64.close();
  218. connection.setRequestProperty("Authorization",
  219. "Basic " + new String(baout.toByteArray(), "UTF-8"));
  220. } catch (IOException e) {
  221. //won't happen. We're operating in-memory.
  222. throw new RuntimeException("Error during base64 encodation of username/password");
  223. }
  224. }
  225. /**
  226. * Parses inline data URIs as generated by MS Word's XML export and FO stylesheet.
  227. * @see <a href="http://www.ietf.org/rfc/rfc2397">RFC 2397</a>
  228. */
  229. private Source parseDataURI(String href) {
  230. int commaPos = href.indexOf(',');
  231. // header is of the form data:[<mediatype>][;base64]
  232. String header = href.substring(0, commaPos);
  233. String data = href.substring(commaPos + 1);
  234. if (header.endsWith(";base64")) {
  235. byte[] bytes = data.getBytes();
  236. ByteArrayInputStream encodedStream = new ByteArrayInputStream(bytes);
  237. Base64DecodeStream decodedStream = new Base64DecodeStream(encodedStream);
  238. return new StreamSource(decodedStream);
  239. } else {
  240. //Note that this is not quite the full story here. But since we are only interested
  241. //in base64-encoded binary data, the next line will probably never be called.
  242. return new StreamSource(new java.io.StringReader(data));
  243. }
  244. }
  245. }