package org.gomba;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.gomba.utils.xml.ContentHandlerUtils;
import org.gomba.utils.xml.ObjectInputSource;
import org.gomba.utils.xml.ObjectXMLReader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Servlet implemetation to be used as an "error page".
*
* <dl>
*
* <dt>doctype-public</dt>
* <dd>Specifies the public identifier to be used in the document type
* declaration. If the doctype-system init-param is not set, then the
* doctype-public init-param is ignored. (Optional)</dd>
*
* <dt>doctype-system</dt>
* <dd>Specifies the system identifier to be used in the document type
* declaration. (Optional)</dd>
*
* <dt>media-type</dt>
* <dd>The resource MIME content type. Defaults to "text/xml" (Optional)</dd>
*
* <dt>xslt</dt>
* <dd>The XSLT stylesheet to apply to the default XML output. (Optional)</dd>
*
* <dt>xslt-params</dt>
* <dd>XSLT parameters in Java Properties format. (Optional)</dd>
*
* </dl>
*
* @author Flavio Tordini
* @version $Id: ErrorServlet.java,v 1.2 2005/07/06 10:25:00 flaviotordini Exp $
*/
public class ErrorServlet extends GenericServlet {
private final static String EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception";
private final static String INIT_PARAM_JAVA_FEATURES = "java-specific";
private final static String DEFAULT_XSLT = "/org/gomba/errorServlet.xslt";
/**
* DTD public identifier, if any.
*/
private String doctypePublic;
/**
* DTD system identifier, if any.
*/
private String doctypeSystem;
/**
* The resource MIME content type, if any.
*/
private String mediaType;
/**
* The parsed XSLT stylesheet, if any.
*/
private Templates templates;
/**
* XSLT parameters.
*/
private Map xsltFixedParameters;
/**
* Wheter to include Java-specific information in the error XML
*/
private boolean javaFeatures;
/**
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// Java specific features
String javaFeaturesStr = config
.getInitParameter(INIT_PARAM_JAVA_FEATURES);
if (javaFeaturesStr != null) {
this.javaFeatures = Boolean.valueOf(javaFeaturesStr).booleanValue();
}
// DTD
this.doctypePublic = config.getInitParameter("doctype-public");
this.doctypeSystem = config.getInitParameter("doctype-system");
// MIME
this.mediaType = config.getInitParameter("media-type");
// XSLT
final String xsltStyleSheet = config.getInitParameter("xslt");
InputStream is;
if (xsltStyleSheet != null) {
// Create a templates object, which is the processed,
// thread-safe representation of the stylesheet.
is = getServletContext().getResourceAsStream(xsltStyleSheet);
} else {
is = ErrorServlet.class.getResourceAsStream(DEFAULT_XSLT);
}
if (is == null) {
throw new ServletException("Cannot find stylesheet: "
+ xsltStyleSheet);
}
try {
TransformerFactory tfactory = TransformerFactory.newInstance();
Source xslSource = new StreamSource(is);
if (xsltStyleSheet != null) {
// Note that if we don't do this, relative URLs can not be
// resolved correctly!
xslSource.setSystemId(getServletContext().getRealPath(
xsltStyleSheet));
}
this.templates = tfactory.newTemplates(xslSource);
} catch (TransformerConfigurationException tce) {
throw new ServletException("Error parsing XSLT stylesheet: "
+ xsltStyleSheet, tce);
}
// create a map of fixed xslt parameters
final String xsltParams = config.getInitParameter("xslt-params");
if (xsltParams != null) {
try {
this.xsltFixedParameters = buildXsltFixedParams(xsltParams);
} catch (Exception e) {
throw new ServletException("Error parsing XSLT params: "
+ xsltParams, e);
}
}
}
private static Map buildXsltFixedParams(String xsltParams)
throws IOException {
Properties parameters = new Properties();
InputStream inputStream = new ByteArrayInputStream(xsltParams
.getBytes());
try {
parameters.load(inputStream);
} finally {
inputStream.close();
}
return parameters;
}
/**
* @see javax.servlet.Servlet#service(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse)
*/
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
// get the exception for the request
Throwable throwable = (Throwable) request
.getAttribute(EXCEPTION_ATTRIBUTE);
if (throwable == null) {
throw new ServletException("Missing " + EXCEPTION_ATTRIBUTE
+ " request attribute");
}
// generate XML
try {
serializeXML(throwable, response);
} catch (Exception e) {
throw new ServletException(
"Error rendering error XML representation.", e);
}
}
/**
* Serialize a <code>Throwable</code> object to XML using SAX and TrAX
* APIs in a smart way. Dagnelo, you're a sucker!
*
* @param throwable
* The <code>Throwable</code> object to serialize
* @param response
* The HTTP response
* @see <a
* href="http://java.sun.com/j2se/1.4.2/docs/api/javax/xml/transform/package-summary.html">TrAX
* API </a>
*/
private void serializeXML(Throwable throwable, ServletResponse response)
throws Exception {
// Let the HTTP client know the output content type
if (this.mediaType != null) {
response.setContentType(this.mediaType);
} else {
response.setContentType("text/xml");
}
// Create TrAX Transformer
Transformer t;
if (this.templates != null) {
// Create a transformer using our stylesheet
t = this.templates.newTransformer();
// pass fixed XSLT parameters
if (this.xsltFixedParameters != null) {
for (Iterator i = this.xsltFixedParameters.entrySet()
.iterator(); i.hasNext();) {
Map.Entry mapEntry = (Map.Entry) i.next();
t.setParameter((String) mapEntry.getKey(), mapEntry
.getValue());
}
}
// TODO maybe we could also pass some dynamic values such as the
// request param or path info. But let's wait until the need
// arises...
} else {
// Create an "identity" transformer - copies input to output
t = TransformerFactory.newInstance().newTransformer();
}
// Set trasformation output properties
t.setOutputProperty(OutputKeys.ENCODING, response
.getCharacterEncoding());
// DTD
if (this.doctypePublic != null) {
t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, this.doctypePublic);
}
if (this.doctypeSystem != null) {
t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, this.doctypeSystem);
}
// Create the trasformation source using our custom ObjectInputSource
InputSource inputSource = new ObjectInputSource(throwable);
// Create the sax "parser".
ObjectXMLReader saxReader = new ErrorRequestXMLReader();
Source source = new SAXSource(saxReader, inputSource);
// Create the trasformation result
Result result = new StreamResult(response.getWriter());
// Result result = new StreamResult(response.getOutputStream());
// Go!
t.transform(source, result);
}
/**
* This SAX XMLReader generates an empty XML document. This is used for
* generating a dummy default document.
*/
final class ErrorRequestXMLReader extends ObjectXMLReader {
private final static String ROOT_ELEMENT = "error";
private final static String STACK_TRACE_ELEMENT = "stackTrace";
private final static String STACK_TRACE_ITEM_ELEMENT = "element";
/**
* @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
*/
public void parse(ObjectInputSource input) throws IOException,
SAXException {
Throwable throwable = (Throwable) input.getObject();
this.handler.startDocument();
exceptionEvents(throwable, null);
this.handler.endDocument();
}
/**
* Recursive method that fires SAX events for a Throwable object and its
* causes.
*/
private void exceptionEvents(Throwable throwable,
StackTraceElement[] parentStrackTrace) throws SAXException {
this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
ROOT_ELEMENT, ROOT_ELEMENT, ContentHandlerUtils.DUMMY_ATTS);
ContentHandlerUtils.tag(this.handler, "message", throwable
.getMessage());
StackTraceElement[] ste = null;
if (ErrorServlet.this.javaFeatures) {
ContentHandlerUtils.tag(this.handler, "type", throwable
.getClass().getName());
// stack trace
ste = throwable.getStackTrace();
// limit stack trace the same way printStackTrace() does
// lifted and adapted from java.lang.Throwable
// let's hope Sun does not sue me!
int usefulFrames = ste.length - 1;
if (parentStrackTrace != null) {
// Compute number of frames in common between this and
// caused exception
int n = parentStrackTrace.length - 1;
while (usefulFrames >= 0 && n >= 0
&& ste[usefulFrames].equals(parentStrackTrace[n])) {
usefulFrames--;
n--;
}
}
this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
STACK_TRACE_ELEMENT, STACK_TRACE_ELEMENT,
ContentHandlerUtils.DUMMY_ATTS);
for (int i = 0; i <= usefulFrames; i++) {
this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
STACK_TRACE_ITEM_ELEMENT, STACK_TRACE_ITEM_ELEMENT,
ContentHandlerUtils.DUMMY_ATTS);
ContentHandlerUtils.tag(this.handler, "type", ste[i]
.getClassName());
ContentHandlerUtils.tag(this.handler, "method", ste[i]
.getMethodName());
ContentHandlerUtils.tag(this.handler, "file", ste[i]
.getFileName());
ContentHandlerUtils.tag(this.handler, "line", Integer
.toString(ste[i].getLineNumber()));
if (ste[i].isNativeMethod()) {
ContentHandlerUtils.tag(this.handler, "native", Boolean
.toString(ste[i].isNativeMethod()));
}
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
STACK_TRACE_ITEM_ELEMENT, STACK_TRACE_ITEM_ELEMENT);
}
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
STACK_TRACE_ELEMENT, STACK_TRACE_ELEMENT);
}
// recurse!
Throwable cause = throwable.getCause();
if (cause != null) {
exceptionEvents(cause, ste);
}
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
ROOT_ELEMENT, ROOT_ELEMENT);
}
}
}