/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package org.fcrepo.localservices.saxon;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.CoreConnectionPNames;
import net.sf.saxon.FeatureKeys;
import net.sf.saxon.value.StringValue;
/**
* A service that transforms a supplied input document using a supplied
* stylesheet, with stylesheet caching. Adapted from the SaxonServlet.java
* example file contained in the source distribution of "The SAXON XSLT
* Processor from Michael Kay".
*
* <pre>
* -----------------------------------------------------------------------------
* The original code is Copyright © 2001 by Michael Kay. All rights
* reserved. The current project homepage for Saxon may be found at:
* <a href="http://saxon.sourceforge.net/">http://saxon.sourceforge.net/</a>.
*
* Portions created for the Fedora Repository System are Copyright © 2002-2007
* by The Rector and Visitors of the University of Virginia and Cornell
* University. All rights reserved.
* -----------------------------------------------------------------------------
* </pre>
*
* @author Michael Kay
* @author Ross Wayland
* @author Chris Wilper
* @version $Id$
*/
public class SaxonServlet
extends HttpServlet {
private static final long serialVersionUID = 2L;
/** time to wait for getting data via http before giving up */
public final int TIMEOUT_SECONDS = 10;
/** start string for a servlet config parameter name that gives creds */
private final String CRED_PARAM_START = "credentials for ";
/** urlString-to-Templates map of cached stylesheets */
private Map<String, Templates> m_cache;
/** pathString-to-Credentials map of configured credentials */
private Map<String, UsernamePasswordCredentials> m_creds;
/** provider of http connections */
private PoolingClientConnectionManager m_cManager;
/**
* Initialize the servlet by setting up the stylesheet cache, the http
* connection manager, and configuring credentials for the http client.
*/
@Override
public void init(ServletConfig config) throws ServletException {
m_cache = new HashMap<String, Templates>();
m_creds = new HashMap<String, UsernamePasswordCredentials>();
m_cManager = new PoolingClientConnectionManager();
m_cManager.getSchemeRegistry().register(
new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));
m_cManager.getSchemeRegistry().register(
new Scheme("https-tomcat", 8443, SSLSocketFactory.getSocketFactory()));
m_cManager.getSchemeRegistry().register(
new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
m_cManager.getSchemeRegistry().register(
new Scheme("http-tomcat", 8080, PlainSocketFactory.getSocketFactory()));
Enumeration<?> enm = config.getInitParameterNames();
while (enm.hasMoreElements()) {
String name = (String) enm.nextElement();
if (name.startsWith(CRED_PARAM_START)) {
String value = config.getInitParameter(name);
if (value.indexOf(":") == -1) {
throw new ServletException("Malformed credentials for "
+ name + " -- expected ':' user/pass delimiter");
}
String[] parts = value.split(":");
String user = parts[0];
StringBuffer pass = new StringBuffer();
for (int i = 1; i < parts.length; i++) {
if (i > 1) {
pass.append(':');
}
pass.append(parts[i]);
}
m_creds.put(name.substring(CRED_PARAM_START.length()),
new UsernamePasswordCredentials(user, pass
.toString()));
}
}
}
@Override
public void destroy() {
m_cManager.shutdown();
}
/**
* Accept a GET request and produce a response. HTTP Request Parameters:
* <ul>
* <li>source - URL of source document</li>
* <li>style - URL of stylesheet</li>
* <li>clear-stylesheet-cache - if set to yes, empties the cache before
* running.
* </ul>
*
* @param req
* The HTTP request
* @param res
* The HTTP response
*/
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
String source = req.getParameter("source");
String style = req.getParameter("style");
String clear = req.getParameter("clear-stylesheet-cache");
if (clear != null && clear.equals("yes")) {
m_cache.remove(style);
}
try {
apply(style, source, req, res);
} catch (Exception e) {
res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e
.getMessage());
e.printStackTrace();
}
}
/**
* Accept an POST request and produce a response (same behavior as GET).
*/
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException {
doGet(req, res);
}
@Override
public String getServletInfo() {
return "Calls SAXON to apply a stylesheet to a source document";
}
/**
* Apply stylesheet to source document
*/
private void apply(String style,
String source,
HttpServletRequest req,
HttpServletResponse res) throws Exception {
// Validate parameters
if (style == null) {
throw new TransformerException("No style parameter supplied");
}
if (source == null) {
throw new TransformerException("No source parameter supplied");
}
InputStream sourceStream = null;
try {
// Load the stylesheet (adding to cache if necessary)
Templates pss = tryCache(style);
Transformer transformer = pss.newTransformer();
Enumeration<?> p = req.getParameterNames();
while (p.hasMoreElements()) {
String name = (String) p.nextElement();
if (!(name.equals("style") || name.equals("source"))) {
String value = req.getParameter(name);
transformer.setParameter(name, new StringValue(value));
}
}
// Start loading the document to be transformed
sourceStream = getInputStream(source);
// Set the appropriate output mime type
String mime =
pss.getOutputProperties()
.getProperty(OutputKeys.MEDIA_TYPE);
if (mime == null) {
res.setContentType("text/html");
} else {
res.setContentType(mime);
}
// Transform
StreamSource ss = new StreamSource(sourceStream);
ss.setSystemId(source);
transformer.transform(ss, new StreamResult(res.getOutputStream()));
} finally {
if (sourceStream != null) {
try {
sourceStream.close();
} catch (Exception e) {
}
}
}
}
/**
* Maintain prepared stylesheets in memory for reuse
*/
private Templates tryCache(String url) throws Exception {
Templates x = (Templates) m_cache.get(url);
if (x == null) {
synchronized (m_cache) {
if (!m_cache.containsKey(url)) {
TransformerFactory factory = TransformerFactory.newInstance();
if (factory.getClass().getName().equals("net.sf.saxon.TransformerFactoryImpl")) {
factory.setAttribute(FeatureKeys.VERSION_WARNING, Boolean.FALSE);
}
StreamSource ss = new StreamSource(getInputStream(url));
ss.setSystemId(url);
x = factory.newTemplates(ss);
m_cache.put(url, x);
}
}
}
return x;
}
/**
* Get the content at the given location using the configured credentials
* (if any).
*/
private InputStream getInputStream(String url) throws Exception {
HttpGet getMethod = new HttpGet(url);
DefaultHttpClient client = new DefaultHttpClient(m_cManager);
client.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, TIMEOUT_SECONDS * 1000);
UsernamePasswordCredentials creds = getCreds(url);
if (creds != null) {
client.getCredentialsProvider().setCredentials(AuthScope.ANY, creds);
client.addRequestInterceptor(new PreemptiveAuth());
}
client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, true);
HttpInputStream in = new HttpInputStream(client, getMethod);
if (in.getStatusCode() != 200) {
try {
in.close();
} catch (Exception e) {
}
throw new IOException("HTTP request failed. Got status code "
+ in.getStatusCode()
+ " from remote server while attempting to GET " + url);
} else {
return in;
}
}
/**
* Return the credentials for the realmPath that most closely matches the
* given url, or null if none found.
*/
private UsernamePasswordCredentials getCreds(String url) throws Exception {
url = normalizeURL(url);
url = url.substring(url.indexOf("/") + 2);
UsernamePasswordCredentials longestMatch = null;
int longestMatchLength = 0;
Iterator<String> iter = m_creds.keySet().iterator();
while (iter.hasNext()) {
String realmPath = (String) iter.next();
if (url.startsWith(realmPath)) {
int matchLength = realmPath.length();
if (matchLength > longestMatchLength) {
longestMatchLength = matchLength;
longestMatch =
(UsernamePasswordCredentials) m_creds
.get(realmPath);
}
}
}
return longestMatch;
}
/**
* Return a URL string in which the port is always specified.
*/
private static String normalizeURL(String urlString)
throws MalformedURLException {
URL url = new URL(urlString);
if (url.getPort() == -1) {
return url.getProtocol() + "://" + url.getHost() + ":"
+ url.getDefaultPort() + url.getFile()
+ (url.getRef() != null ? "#" + url.getRef() : "");
} else {
return urlString;
}
}
}