001: package org.gomba;
002:
003: import java.io.ByteArrayInputStream;
004: import java.io.IOException;
005: import java.io.InputStream;
006: import java.util.Iterator;
007: import java.util.Map;
008: import java.util.Properties;
009:
010: import javax.servlet.GenericServlet;
011: import javax.servlet.ServletConfig;
012: import javax.servlet.ServletException;
013: import javax.servlet.ServletRequest;
014: import javax.servlet.ServletResponse;
015: import javax.xml.transform.OutputKeys;
016: import javax.xml.transform.Result;
017: import javax.xml.transform.Source;
018: import javax.xml.transform.Templates;
019: import javax.xml.transform.Transformer;
020: import javax.xml.transform.TransformerConfigurationException;
021: import javax.xml.transform.TransformerFactory;
022: import javax.xml.transform.sax.SAXSource;
023: import javax.xml.transform.stream.StreamResult;
024: import javax.xml.transform.stream.StreamSource;
025:
026: import org.gomba.utils.xml.ContentHandlerUtils;
027: import org.gomba.utils.xml.ObjectInputSource;
028: import org.gomba.utils.xml.ObjectXMLReader;
029: import org.xml.sax.InputSource;
030: import org.xml.sax.SAXException;
031:
032: /**
033: * Servlet implemetation to be used as an "error page".
034: *
035: * <dl>
036: *
037: * <dt>doctype-public</dt>
038: * <dd>Specifies the public identifier to be used in the document type
039: * declaration. If the doctype-system init-param is not set, then the
040: * doctype-public init-param is ignored. (Optional)</dd>
041: *
042: * <dt>doctype-system</dt>
043: * <dd>Specifies the system identifier to be used in the document type
044: * declaration. (Optional)</dd>
045: *
046: * <dt>media-type</dt>
047: * <dd>The resource MIME content type. Defaults to "text/xml" (Optional)</dd>
048: *
049: * <dt>xslt</dt>
050: * <dd>The XSLT stylesheet to apply to the default XML output. (Optional)</dd>
051: *
052: * <dt>xslt-params</dt>
053: * <dd>XSLT parameters in Java Properties format. (Optional)</dd>
054: *
055: * </dl>
056: *
057: * @author Flavio Tordini
058: * @version $Id: ErrorServlet.java,v 1.2 2005/07/06 10:25:00 flaviotordini Exp $
059: */
060: public class ErrorServlet extends GenericServlet {
061:
062: private final static String EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception";
063:
064: private final static String INIT_PARAM_JAVA_FEATURES = "java-specific";
065:
066: private final static String DEFAULT_XSLT = "/org/gomba/errorServlet.xslt";
067:
068: /**
069: * DTD public identifier, if any.
070: */
071: private String doctypePublic;
072:
073: /**
074: * DTD system identifier, if any.
075: */
076: private String doctypeSystem;
077:
078: /**
079: * The resource MIME content type, if any.
080: */
081: private String mediaType;
082:
083: /**
084: * The parsed XSLT stylesheet, if any.
085: */
086: private Templates templates;
087:
088: /**
089: * XSLT parameters.
090: */
091: private Map xsltFixedParameters;
092:
093: /**
094: * Wheter to include Java-specific information in the error XML
095: */
096: private boolean javaFeatures;
097:
098: /**
099: * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
100: */
101: public void init(ServletConfig config) throws ServletException {
102: super .init(config);
103:
104: // Java specific features
105: String javaFeaturesStr = config
106: .getInitParameter(INIT_PARAM_JAVA_FEATURES);
107: if (javaFeaturesStr != null) {
108: this .javaFeatures = Boolean.valueOf(javaFeaturesStr)
109: .booleanValue();
110: }
111:
112: // DTD
113: this .doctypePublic = config.getInitParameter("doctype-public");
114: this .doctypeSystem = config.getInitParameter("doctype-system");
115:
116: // MIME
117: this .mediaType = config.getInitParameter("media-type");
118:
119: // XSLT
120: final String xsltStyleSheet = config.getInitParameter("xslt");
121:
122: InputStream is;
123: if (xsltStyleSheet != null) {
124: // Create a templates object, which is the processed,
125: // thread-safe representation of the stylesheet.
126: is = getServletContext()
127: .getResourceAsStream(xsltStyleSheet);
128: } else {
129: is = ErrorServlet.class.getResourceAsStream(DEFAULT_XSLT);
130: }
131:
132: if (is == null) {
133: throw new ServletException("Cannot find stylesheet: "
134: + xsltStyleSheet);
135: }
136: try {
137: TransformerFactory tfactory = TransformerFactory
138: .newInstance();
139: Source xslSource = new StreamSource(is);
140: if (xsltStyleSheet != null) {
141: // Note that if we don't do this, relative URLs can not be
142: // resolved correctly!
143: xslSource.setSystemId(getServletContext().getRealPath(
144: xsltStyleSheet));
145: }
146: this .templates = tfactory.newTemplates(xslSource);
147: } catch (TransformerConfigurationException tce) {
148: throw new ServletException(
149: "Error parsing XSLT stylesheet: " + xsltStyleSheet,
150: tce);
151: }
152:
153: // create a map of fixed xslt parameters
154: final String xsltParams = config
155: .getInitParameter("xslt-params");
156: if (xsltParams != null) {
157: try {
158: this .xsltFixedParameters = buildXsltFixedParams(xsltParams);
159: } catch (Exception e) {
160: throw new ServletException(
161: "Error parsing XSLT params: " + xsltParams, e);
162: }
163: }
164:
165: }
166:
167: private static Map buildXsltFixedParams(String xsltParams)
168: throws IOException {
169: Properties parameters = new Properties();
170:
171: InputStream inputStream = new ByteArrayInputStream(xsltParams
172: .getBytes());
173: try {
174: parameters.load(inputStream);
175: } finally {
176: inputStream.close();
177: }
178:
179: return parameters;
180: }
181:
182: /**
183: * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest,
184: * javax.servlet.ServletResponse)
185: */
186: public void service(ServletRequest request, ServletResponse response)
187: throws ServletException, IOException {
188:
189: // get the exception for the request
190: Throwable throwable = (Throwable) request
191: .getAttribute(EXCEPTION_ATTRIBUTE);
192: if (throwable == null) {
193: throw new ServletException("Missing " + EXCEPTION_ATTRIBUTE
194: + " request attribute");
195: }
196:
197: // generate XML
198: try {
199: serializeXML(throwable, response);
200: } catch (Exception e) {
201: throw new ServletException(
202: "Error rendering error XML representation.", e);
203: }
204:
205: }
206:
207: /**
208: * Serialize a <code>Throwable</code> object to XML using SAX and TrAX
209: * APIs in a smart way. Dagnelo, you're a sucker!
210: *
211: * @param throwable
212: * The <code>Throwable</code> object to serialize
213: * @param response
214: * The HTTP response
215: * @see <a
216: * href="http://java.sun.com/j2se/1.4.2/docs/api/javax/xml/transform/package-summary.html">TrAX
217: * API </a>
218: */
219: private void serializeXML(Throwable throwable,
220: ServletResponse response) throws Exception {
221:
222: // Let the HTTP client know the output content type
223: if (this .mediaType != null) {
224: response.setContentType(this .mediaType);
225: } else {
226: response.setContentType("text/xml");
227: }
228:
229: // Create TrAX Transformer
230: Transformer t;
231: if (this .templates != null) {
232: // Create a transformer using our stylesheet
233: t = this .templates.newTransformer();
234:
235: // pass fixed XSLT parameters
236: if (this .xsltFixedParameters != null) {
237: for (Iterator i = this .xsltFixedParameters.entrySet()
238: .iterator(); i.hasNext();) {
239: Map.Entry mapEntry = (Map.Entry) i.next();
240: t.setParameter((String) mapEntry.getKey(), mapEntry
241: .getValue());
242: }
243: }
244:
245: // TODO maybe we could also pass some dynamic values such as the
246: // request param or path info. But let's wait until the need
247: // arises...
248:
249: } else {
250: // Create an "identity" transformer - copies input to output
251: t = TransformerFactory.newInstance().newTransformer();
252: }
253:
254: // Set trasformation output properties
255: t.setOutputProperty(OutputKeys.ENCODING, response
256: .getCharacterEncoding());
257: // DTD
258: if (this .doctypePublic != null) {
259: t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
260: this .doctypePublic);
261: }
262: if (this .doctypeSystem != null) {
263: t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
264: this .doctypeSystem);
265: }
266:
267: // Create the trasformation source using our custom ObjectInputSource
268: InputSource inputSource = new ObjectInputSource(throwable);
269:
270: // Create the sax "parser".
271: ObjectXMLReader saxReader = new ErrorRequestXMLReader();
272:
273: Source source = new SAXSource(saxReader, inputSource);
274:
275: // Create the trasformation result
276: Result result = new StreamResult(response.getWriter());
277: // Result result = new StreamResult(response.getOutputStream());
278:
279: // Go!
280: t.transform(source, result);
281:
282: }
283:
284: /**
285: * This SAX XMLReader generates an empty XML document. This is used for
286: * generating a dummy default document.
287: */
288: final class ErrorRequestXMLReader extends ObjectXMLReader {
289:
290: private final static String ROOT_ELEMENT = "error";
291:
292: private final static String STACK_TRACE_ELEMENT = "stackTrace";
293:
294: private final static String STACK_TRACE_ITEM_ELEMENT = "element";
295:
296: /**
297: * @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
298: */
299: public void parse(ObjectInputSource input) throws IOException,
300: SAXException {
301:
302: Throwable throwable = (Throwable) input.getObject();
303:
304: this .handler.startDocument();
305: exceptionEvents(throwable, null);
306: this .handler.endDocument();
307: }
308:
309: /**
310: * Recursive method that fires SAX events for a Throwable object and its
311: * causes.
312: */
313: private void exceptionEvents(Throwable throwable,
314: StackTraceElement[] parentStrackTrace)
315: throws SAXException {
316: this .handler.startElement(ContentHandlerUtils.DUMMY_NSU,
317: ROOT_ELEMENT, ROOT_ELEMENT,
318: ContentHandlerUtils.DUMMY_ATTS);
319:
320: ContentHandlerUtils.tag(this .handler, "message", throwable
321: .getMessage());
322:
323: StackTraceElement[] ste = null;
324: if (ErrorServlet.this .javaFeatures) {
325:
326: ContentHandlerUtils.tag(this .handler, "type", throwable
327: .getClass().getName());
328:
329: // stack trace
330: ste = throwable.getStackTrace();
331:
332: // limit stack trace the same way printStackTrace() does
333: // lifted and adapted from java.lang.Throwable
334: // let's hope Sun does not sue me!
335: int usefulFrames = ste.length - 1;
336: if (parentStrackTrace != null) {
337: // Compute number of frames in common between this and
338: // caused exception
339: int n = parentStrackTrace.length - 1;
340: while (usefulFrames >= 0
341: && n >= 0
342: && ste[usefulFrames]
343: .equals(parentStrackTrace[n])) {
344: usefulFrames--;
345: n--;
346: }
347: }
348:
349: this .handler.startElement(
350: ContentHandlerUtils.DUMMY_NSU,
351: STACK_TRACE_ELEMENT, STACK_TRACE_ELEMENT,
352: ContentHandlerUtils.DUMMY_ATTS);
353:
354: for (int i = 0; i <= usefulFrames; i++) {
355:
356: this .handler.startElement(
357: ContentHandlerUtils.DUMMY_NSU,
358: STACK_TRACE_ITEM_ELEMENT,
359: STACK_TRACE_ITEM_ELEMENT,
360: ContentHandlerUtils.DUMMY_ATTS);
361:
362: ContentHandlerUtils.tag(this .handler, "type",
363: ste[i].getClassName());
364:
365: ContentHandlerUtils.tag(this .handler, "method",
366: ste[i].getMethodName());
367:
368: ContentHandlerUtils.tag(this .handler, "file",
369: ste[i].getFileName());
370:
371: ContentHandlerUtils.tag(this .handler, "line",
372: Integer.toString(ste[i].getLineNumber()));
373:
374: if (ste[i].isNativeMethod()) {
375: ContentHandlerUtils.tag(this .handler, "native",
376: Boolean.toString(ste[i]
377: .isNativeMethod()));
378: }
379:
380: this .handler.endElement(
381: ContentHandlerUtils.DUMMY_NSU,
382: STACK_TRACE_ITEM_ELEMENT,
383: STACK_TRACE_ITEM_ELEMENT);
384:
385: }
386:
387: this .handler.endElement(ContentHandlerUtils.DUMMY_NSU,
388: STACK_TRACE_ELEMENT, STACK_TRACE_ELEMENT);
389:
390: }
391:
392: // recurse!
393: Throwable cause = throwable.getCause();
394: if (cause != null) {
395: exceptionEvents(cause, ste);
396: }
397:
398: this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
399: ROOT_ELEMENT, ROOT_ELEMENT);
400: }
401: }
402:
403: }
|