/*
* Carrot2 project.
*
* Copyright (C) 2002-2016, Dawid Weiss, Stanisław Osiński.
* All rights reserved.
*
* Refer to the full license file "carrot2.LICENSE"
* in the root folder of the repository checkout or at:
* http://www.carrot2.org/carrot2.LICENSE
*/
package org.carrot2.util.xsltfilter;
import java.io.*;
import java.util.Map;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import org.slf4j.Logger;
import org.carrot2.util.xslt.TemplatesPool;
import org.xml.sax.*;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* A wrapper around a {@link HttpServletResponse} which attempts to detect the type of
* output acquired from the servlet chain and apply a stylesheet to it if all conditions
* mentioned in {@link XSLTFilter} are met.
*/
final class XSLTFilterServletResponse extends HttpServletResponseWrapper
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(XSLTFilterServletResponse.class);
/**
* If true, the stream will be passed verbatim to the next filter. This usually
* happens when the output has a mime type different than <code>text/xml</code>.
*/
private boolean passthrough;
/**
* The actual {@link HttpServletResponse}.
*/
private HttpServletResponse origResponse = null;
/**
* The actual {@link HttpServletRequest}.
*/
private HttpServletRequest origRequest;
/**
* The {@link ServletOutputStream} returned from {@link #getOutputStream()} or
* <code>null</code>.
*/
private ServletOutputStream stream = null;
/**
* The {@link PrintWriter} returned from {@link #getWriter()} or <code>null</code>.
*/
private PrintWriter writer = null;
/**
* A pool of stylesheets used for XSLT processing.
*/
private TemplatesPool transformers;
/**
* Servlet context for resolving local paths.
*/
private ServletContext context;
/**
* Creates an XSLT filter servlet response for a single request, wrapping a given
* {@link HttpServletResponse}.
*
* @param response The original chain's {@link HttpServletResponse}.
* @param request The original chain's {@link HttpServletRequest}.
* @param transformers A pool of transformers to be used with this request.
*/
public XSLTFilterServletResponse(HttpServletResponse response,
HttpServletRequest request, ServletContext context, TemplatesPool transformers)
{
super(response);
this.origResponse = response;
this.transformers = transformers;
this.origRequest = request;
this.context = context;
}
/**
* We override this method to detect XML data streams.
*/
public void setContentType(String contentType)
{
// Check if XSLT processing has been suppressed for this request.
final boolean processingSuppressed = processingSuppressed(origRequest);
if (processingSuppressed)
{
// Processing is suppressed.
log.debug("XSLT processing disabled for the request.");
}
if (!processingSuppressed
&& (contentType.startsWith("text/xml") ||
contentType.startsWith("application/xml")))
{
/*
* We have an XML data stream. Do not enforce the content type. If needed,
* the XSLT stylesheet can override it via the xsl:output instruction.
*/
origResponse.setContentType(contentType);
}
else
{
/*
* The input is something we won't process anyway, so simply passthrough all
* data directly to the output stream.
*/
if (!processingSuppressed)
{
log.info("Content type is not text/xml or application/xml ("
+ contentType + "), passthrough.");
}
origResponse.setContentType(contentType);
passthrough = true;
// If the output stream is already initialized, passthrough everything.
if (stream != null && stream instanceof DeferredOutputStream)
{
try
{
((DeferredOutputStream) stream).passthrough(origResponse
.getOutputStream());
}
catch (IOException e)
{
((DeferredOutputStream) stream).setExceptionOnNext(e);
}
}
}
}
/**
* Return <code>true</code> if the original request contained XSLT suppressing key.
*
* @see XSLTFilterConstants#NO_XSLT_PROCESSING
*/
private boolean processingSuppressed(HttpServletRequest origRequest2)
{
return (origRequest.getAttribute(XSLTFilterConstants.NO_XSLT_PROCESSING) != null)
|| (origRequest.getParameter(XSLTFilterConstants.NO_XSLT_PROCESSING) != null);
}
/**
* We do not delegate content length because it will most likely change.
*/
public void setContentLength(final int length)
{
log.debug("Original content length (ignored): " + length);
}
/**
* Flush the internal buffers. This only works if XSLT transformation is suppressed.
*/
public void flushBuffer() throws IOException
{
if (stream != null) {
this.stream.flush();
}
}
/**
* Return the byte output stream for this response. This is either the original stream
* or a buffered stream.
*
* @exception IllegalStateException Thrown when character stream has been already
* initialized ({@link #getWriter()}).
*/
public ServletOutputStream getOutputStream() throws IOException
{
if (writer != null)
{
throw new IllegalStateException(
"Character stream has been already initialized. Use streams consequently.");
}
if (stream != null)
{
return stream;
}
if (passthrough)
{
stream = origResponse.getOutputStream();
}
else
{
stream = new DeferredOutputStream();
}
return stream;
}
/**
* Return the character output stream for this response. This is either the original
* stream or a buffered stream.
*
* @exception IllegalStateException Thrown when byte stream has been already
* initialized ({@link #getOutputStream()}).
*/
public PrintWriter getWriter() throws IOException
{
if (stream != null)
{
throw new IllegalStateException(
"Byte stream has been already initialized. Use streams consequently.");
}
if (writer != null)
{
return writer;
}
if (passthrough)
{
writer = this.origResponse.getWriter();
return writer;
}
/*
* TODO: The character encoding should be extracted in {@link #setContentType()},
* saved somewhere locally and used here. The response's character encoding may be
* different (depends on the stylesheet).
*/
final String charEnc = origResponse.getCharacterEncoding();
this.stream = new DeferredOutputStream();
if (charEnc != null)
{
writer = new PrintWriter(new OutputStreamWriter(stream, charEnc));
}
else
{
writer = new PrintWriter(stream);
}
return writer;
}
/**
* This method must be invoked at the end of processing. The streams are closed and
* their content is analyzed. Actual XSLT processing takes place here.
*/
@SuppressWarnings("unchecked")
void finishResponse() throws IOException
{
if (writer != null)
{
writer.close();
}
else
{
if (stream != null) stream.close();
}
/*
* If we're not in passthrough mode, then we need to finalize XSLT transformation.
*/
if (false == passthrough)
{
if (stream != null)
{
final byte [] bytes = ((DeferredOutputStream) stream).getBytes();
final boolean processingSuppressed = (origRequest
.getAttribute(XSLTFilterConstants.NO_XSLT_PROCESSING) != null)
|| (origRequest.getParameter(XSLTFilterConstants.NO_XSLT_PROCESSING) != null);
if (processingSuppressed)
{
// Just copy the buffered data to the output directly.
final OutputStream os = origResponse.getOutputStream();
os.write(bytes);
os.close();
}
else
{
// Otherwise apply XSLT transformation to it.
try
{
processWithXslt(
bytes,
(Map<String, Object>) origRequest.getAttribute(XSLTFilterConstants.XSLT_PARAMS_MAP),
origResponse);
}
catch (TransformerException e)
{
final Throwable t = unwrapCause(e);
if (t instanceof IOException)
{
throw (IOException) t;
}
filterError("Error applying stylesheet.", e);
}
}
}
}
}
/**
* Unwraps original throwable from the transformer/ SAX stack.
*/
private Throwable unwrapCause(TransformerException e)
{
Throwable t;
if (e.getException() != null)
{
t = e.getException();
}
else if (e.getCause() != null)
{
t = e.getCause();
}
else
{
return e;
}
do
{
if (t instanceof IOException)
{
// break early on IOException
return t;
}
else if (t.getCause() != null)
{
t = t.getCause();
}
else if (t instanceof SAXException
&& ((SAXException) t).getException() != null)
{
t = ((SAXException) t).getException();
}
else
{
return t;
}
}
while (true);
}
/**
* Process the byte array (input XML) with the XSLT stylesheet and push the result to
* the output stream.
*/
private void processWithXslt(byte [] bytes,
final Map<String, Object> stylesheetParams, final HttpServletResponse response)
throws TransformerConfigurationException, TransformerException, IOException
{
final TransformingDocumentHandler docHandler;
try
{
final XMLReader reader = XMLReaderFactory.createXMLReader();
docHandler = new TransformingDocumentHandler(origRequest, context,
stylesheetParams, transformers);
docHandler.setContentTypeListener(new IContentTypeListener()
{
public void setContentType(String contentType, String encoding)
{
if (encoding == null)
{
response.setContentType(contentType);
}
else
{
response.setContentType(contentType + "; charset=" + encoding);
}
try
{
docHandler.setTransformationResult(new StreamResult(response
.getOutputStream()));
}
catch (IOException e)
{
throw new RuntimeException("Could not open output stream.");
}
}
});
reader.setContentHandler(docHandler);
try
{
reader.parse(new InputSource(new ByteArrayInputStream(bytes)));
}
finally
{
docHandler.cleanup();
}
}
catch (SAXException e)
{
if (log.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
char [] hex = "0123456789abcdef".toCharArray();
for (int i = 0; i < bytes.length; i++) {
int c = bytes[i] & 0xff;
sb.append(hex[c >>> 4]);
sb.append(hex[c & 0xf]);
}
log.debug("Failed to parse the following input (hex-encoded): "
+ sb.toString(), e);
}
final Exception nested = e.getException();
if (nested != null)
{
if (nested instanceof IOException)
{
throw (IOException) nested;
}
else if (nested instanceof TransformerException)
{
throw (TransformerException) nested;
}
}
throw new TransformerException("Input parsing exception.", e);
}
}
/**
* Attempts to send an internal server error HTTP error, if possible. Otherwise simply
* pushes the exception message to the output stream.
*
* @param message Message to be printed to the logger and to the output stream.
* @param t Exception that caused the error.
*/
protected void filterError(String message, Throwable t)
{
log.error("XSLT filter error: " + message, t);
if (false == origResponse.isCommitted())
{
// Reset the buffer and previous status code.
origResponse.reset();
origResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
origResponse.setContentType("text/html; charset=UTF-8");
}
// Response committed. Just push the error to the output stream.
try
{
final OutputStream os = origResponse.getOutputStream();
final PrintWriter osw = new PrintWriter(new OutputStreamWriter(os,
"iso8859-1"));
osw.write("<html><body><!-- " + XSLTFilterConstants.ERROR_TOKEN + " -->");
osw.write("<h1 style=\"color: red; margin-top: 1em;\">");
osw.write("Internal server exception");
osw.write("</h1>");
osw.write("<b>URI</b>: " + origRequest.getRequestURI() + "\n<br/><br/>");
serializeException(osw, t);
if (t instanceof ServletException
&& ((ServletException) t).getRootCause() != null)
{
osw.write("<br/><br/><h2>ServletException root cause:</h2>");
serializeException(osw, ((ServletException) t).getRootCause());
}
osw.write("</body></html>");
osw.flush();
}
catch (IOException e)
{
// Not much to do in such case (connection broken most likely).
log.debug("Filter error could not be returned to client.");
}
}
/**
* Utility method to serialize an exception and its stack trace to simple HTML.
*/
private final void serializeException(PrintWriter osw, Throwable t)
{
osw.write("<b>Exception</b>: " + t.toString() + "\n<br/><br/>");
osw.write("<b>Stack trace:</b>");
osw
.write("<pre style=\"margin: 1px solid red; padding: 3px; font-family: sans-serif; font-size: small;\">");
t.printStackTrace(osw);
osw.write("</pre>");
}
/**
*
*/
private void detectErrorResponse(int errorCode)
{
if (errorCode != HttpServletResponse.SC_ACCEPTED)
{
origRequest
.setAttribute(XSLTFilterConstants.NO_XSLT_PROCESSING, Boolean.TRUE);
}
}
/**
*
*/
public void sendError(int errorCode) throws IOException
{
detectErrorResponse(errorCode);
super.sendError(errorCode);
}
/**
*
*/
public void sendError(int errorCode, String message) throws IOException
{
detectErrorResponse(errorCode);
super.sendError(errorCode, message);
}
/**
*
*/
public void setStatus(int statusCode)
{
detectErrorResponse(statusCode);
super.setStatus(statusCode);
}
/**
*
*/
public void setStatus(int statusCode, String message)
{
detectErrorResponse(statusCode);
super.setStatus(statusCode, message);
}
}