001: package org.gomba;
002:
003: import java.io.ByteArrayInputStream;
004: import java.io.IOException;
005: import java.io.InputStream;
006: import java.io.Reader;
007: import java.sql.Clob;
008: import java.sql.ResultSet;
009: import java.sql.ResultSetMetaData;
010: import java.sql.SQLException;
011: import java.sql.Timestamp;
012: import java.sql.Types;
013: import java.text.SimpleDateFormat;
014: import java.util.Iterator;
015: import java.util.Map;
016: import java.util.Properties;
017:
018: import javax.servlet.ServletConfig;
019: import javax.servlet.ServletException;
020: import javax.servlet.http.HttpServletRequest;
021: import javax.servlet.http.HttpServletResponse;
022: import javax.xml.transform.OutputKeys;
023: import javax.xml.transform.Result;
024: import javax.xml.transform.Source;
025: import javax.xml.transform.Templates;
026: import javax.xml.transform.Transformer;
027: import javax.xml.transform.TransformerConfigurationException;
028: import javax.xml.transform.TransformerFactory;
029: import javax.xml.transform.sax.SAXSource;
030: import javax.xml.transform.stream.StreamResult;
031: import javax.xml.transform.stream.StreamSource;
032:
033: import org.gomba.utils.xml.ContentHandlerUtils;
034: import org.gomba.utils.xml.ObjectInputSource;
035: import org.gomba.utils.xml.ObjectXMLReader;
036: import org.gomba.utils.xml.XMLTextReader;
037: import org.xml.sax.InputSource;
038: import org.xml.sax.SAXException;
039:
040: /**
041: * Render data accessed via JDBC in XML syntax. The SQL in the
042: * <code>query</code> init-param should be a SELECT and must return a
043: * resultset. This servlet inherits the init-params of
044: * {@link org.gomba.AbstractServlet}, plus:
045: *
046: * <dl>
047: *
048: * <dt>doctype-public</dt>
049: * <dd>Specifies the public identifier to be used in the document type
050: * declaration. If the doctype-system init-param is not set, then the
051: * doctype-public init-param is ignored. (Optional)</dd>
052: *
053: * <dt>doctype-system</dt>
054: * <dd>Specifies the system identifier to be used in the document type
055: * declaration. (Optional)</dd>
056: *
057: * <dt>media-type</dt>
058: * <dd>The resource MIME content type. Defaults to "text/xml" (Optional)</dd>
059: *
060: * <dt>root-element</dt>
061: * <dd>The output XML root element name. Defaults to "resultSet" (Optional)
062: * </dd>
063: *
064: * <dt>row-element</dt>
065: * <dd>The output XML row element name. If an empty value is specified the row
066: * element is omitted. This is useful for queries that always return a single
067: * row. Defaults to "row" (Optional)</dd>
068: *
069: * <dt>xslt</dt>
070: * <dd>The XSLT stylesheet to apply to the default XML output. (Optional)</dd>
071: *
072: * <dt>xslt-params</dt>
073: * <dd>XSLT parameters in Java Properties format. (Optional)</dd>
074: *
075: * <dt>xslt-output-properties</dt>
076: * <dd>XSLT output properties in Java Properties format.
077: * http://www.w3.org/TR/xslt#output (Optional)</dd>
078: *
079: * <dt>column-case</dt>
080: * <dd>The case of XML element names. May be 'lower', 'upper' or 'original'.
081: * Default: 'lower' (Optional)</dd>
082: *
083: * </dl>
084: *
085: * <p>
086: * This servlet can handle the following HTTP methods: GET, HEAD.
087: * </p>
088: *
089: * <p>
090: * The row element children are named after the corresponding JDBC column. If a
091: * column name in your db is not a valid XML element name or you would like to
092: * use a different name in the XML, just use the <code>AS</code> SQL keyword
093: * to change it. It is the user responsibility to create a DTD or XML Schema for
094: * the generated XML.
095: * </p>
096: *
097: * @author Flavio Tordini
098: * @version $Id: XMLServlet.java,v 1.15 2005/12/03 15:47:11 flaviotordini Exp $
099: */
100: public class XMLServlet extends SingleQueryServlet {
101:
102: private final static String ELEMENT_ROOT = "resultSet";
103:
104: private final static String ELEMENT_ROW = "row";
105:
106: private static final String PATTERN_TIMESTAMP = "yyyy-MM-dd'T'HH:mm:ss";
107:
108: private static final String PATTERN_DATE = "yyyy-MM-dd";
109:
110: private static final String PATTERN_TIME = "HH:mm:ss";
111:
112: private static final int CASE_ORIGINAL = 1;
113:
114: private static final int CASE_UPPER = 2;
115:
116: private static final int CASE_LOWER = 3;
117:
118: /**
119: * DTD public identifier, if any.
120: */
121: private String doctypePublic;
122:
123: /**
124: * DTD system identifier, if any.
125: */
126: private String doctypeSystem;
127:
128: /**
129: * The resource MIME content type, if any.
130: */
131: private String mediaType;
132:
133: /**
134: * The parsed XSLT stylesheet, if any.
135: */
136: private Templates templates;
137:
138: /**
139: * The element names.
140: */
141: private String rootElementName, rowElementName;
142:
143: /**
144: * The case of the resultset colums.
145: */
146: private int columnCase;
147:
148: /**
149: * XSLT parameters.
150: */
151: private Map xsltFixedParameters;
152:
153: /**
154: * XSLT output properties.
155: */
156: private Properties xsltOutputProperties;
157:
158: /**
159: * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
160: */
161: public void init(ServletConfig config) throws ServletException {
162: super .init(config);
163:
164: // DTD
165: this .doctypePublic = config.getInitParameter("doctype-public");
166: this .doctypeSystem = config.getInitParameter("doctype-system");
167:
168: // MIME
169: this .mediaType = config.getInitParameter("media-type");
170:
171: // column case
172: String ccase = config.getInitParameter("column-case");
173: if (ccase == null) {
174: // default
175: this .columnCase = CASE_LOWER;
176: } else if (ccase.equals("lower")) {
177: this .columnCase = CASE_LOWER;
178: } else if (ccase.equals("upper")) {
179: this .columnCase = CASE_UPPER;
180: } else if (ccase.equals("original")) {
181: this .columnCase = CASE_ORIGINAL;
182: } else {
183: throw new ServletException("Invalid 'column-case' value: "
184: + ccase);
185: }
186:
187: // element names
188: this .rootElementName = config.getInitParameter("root-element");
189: if (this .rootElementName == null) {
190: this .rootElementName = ELEMENT_ROOT;
191: }
192: this .rowElementName = config.getInitParameter("row-element");
193: if (this .rowElementName == null) {
194: this .rowElementName = ELEMENT_ROW;
195: }
196:
197: // XSLT
198: final String xsltStyleSheet = config.getInitParameter("xslt");
199: if (xsltStyleSheet != null) {
200: // Create a templates object, which is the processed,
201: // thread-safe representation of the stylesheet.
202: InputStream is = getServletContext().getResourceAsStream(
203: xsltStyleSheet);
204: if (is == null) {
205: throw new ServletException("Cannot find stylesheet: "
206: + xsltStyleSheet);
207: }
208: try {
209: TransformerFactory tfactory = TransformerFactory
210: .newInstance();
211: Source xslSource = new StreamSource(is);
212: // Note that if we don't do this, relative URLs can not be
213: // resolved correctly!
214: xslSource.setSystemId(getServletContext().getRealPath(
215: xsltStyleSheet));
216: this .templates = tfactory.newTemplates(xslSource);
217: } catch (TransformerConfigurationException tce) {
218: throw new ServletException(
219: "Error parsing XSLT stylesheet: "
220: + xsltStyleSheet, tce);
221: }
222:
223: // create a map of fixed xslt parameters
224: final String xsltParams = config
225: .getInitParameter("xslt-params");
226: if (xsltParams != null) {
227: try {
228: this .xsltFixedParameters = stringToProperties(xsltParams);
229: } catch (Exception e) {
230: throw new ServletException(
231: "Error parsing XSLT params: " + xsltParams,
232: e);
233: }
234: }
235:
236: // create a map of xslt output properties
237: final String xsltOutputProperties = config
238: .getInitParameter("xslt-output-properties");
239: if (xsltOutputProperties != null) {
240: try {
241: this .xsltOutputProperties = stringToProperties(xsltOutputProperties);
242: } catch (Exception e) {
243: throw new ServletException(
244: "Error parsing XSLT output properties: "
245: + xsltOutputProperties, e);
246: }
247: }
248:
249: }
250:
251: }
252:
253: /**
254: * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
255: * javax.servlet.http.HttpServletResponse)
256: */
257: protected final void doGet(HttpServletRequest request,
258: HttpServletResponse response) throws ServletException,
259: IOException {
260: processRequest(request, response, true);
261: }
262:
263: /**
264: * @see javax.servlet.http.HttpServlet#doHead(javax.servlet.http.HttpServletRequest,
265: * javax.servlet.http.HttpServletResponse)
266: */
267: protected final void doHead(HttpServletRequest request,
268: HttpServletResponse response) throws ServletException,
269: IOException {
270: processRequest(request, response, false);
271: }
272:
273: /**
274: * @see org.gomba.AbstractServlet#doOutput(java.sql.ResultSet,
275: * javax.servlet.http.HttpServletResponse, ParameterResolver)
276: */
277: protected void doOutput(ResultSet resultSet,
278: HttpServletResponse response,
279: ParameterResolver parameterResolver) throws Exception {
280: // Create the sax "parser".
281: ObjectXMLReader saxReader = new ResultSetXMLReader();
282: // generate XML and write it to the response body
283: serializeXML(resultSet, saxReader, response);
284: }
285:
286: /**
287: * @see org.gomba.AbstractServlet#doDefaultOutput(javax.servlet.http.HttpServletResponse)
288: */
289: protected void doDefaultOutput(HttpServletResponse response)
290: throws Exception {
291: // Create a dummy sax "parser".
292: ObjectXMLReader saxReader = new EmptyXMLReader();
293: // generate XML and write it to the response body
294: serializeXML(null, saxReader, response);
295: }
296:
297: /**
298: * Serialize an object to XML using SAX and TrAX APIs in a smart way.
299: *
300: * @param object
301: * The object to serialize
302: * @param saxReader
303: * The SAX "parser"
304: * @param response
305: * The HTTP response
306: * @see <a
307: * href="http://java.sun.com/j2se/1.4.2/docs/api/javax/xml/transform/package-summary.html">TrAX
308: * API </a>
309: */
310: private void serializeXML(Object object, ObjectXMLReader saxReader,
311: HttpServletResponse response) throws Exception {
312:
313: // Let the HTTP client know the output content type
314: String mediaType = null;
315: if (this .mediaType != null) {
316: mediaType = this .mediaType;
317: }
318: if (mediaType == null && this .xsltOutputProperties != null) {
319: mediaType = this .xsltOutputProperties
320: .getProperty(OutputKeys.MEDIA_TYPE);
321: }
322: if (mediaType == null && this .templates != null) {
323: mediaType = this .templates.getOutputProperties()
324: .getProperty(OutputKeys.MEDIA_TYPE);
325: }
326: if (mediaType == null) {
327: mediaType = "text/xml";
328: }
329: response.setContentType(mediaType);
330:
331: // Create TrAX Transformer
332: Transformer t;
333: if (this .templates != null) {
334: // Create a transformer using our stylesheet
335: t = this .templates.newTransformer();
336:
337: // pass fixed XSLT parameters
338: if (this .xsltFixedParameters != null) {
339: for (Iterator i = this .xsltFixedParameters.entrySet()
340: .iterator(); i.hasNext();) {
341: Map.Entry mapEntry = (Map.Entry) i.next();
342: t.setParameter((String) mapEntry.getKey(), mapEntry
343: .getValue());
344: }
345: }
346:
347: // TODO maybe we could also pass some dynamic values such as the
348: // request param or path info. But let's wait until the need
349: // arises...
350:
351: } else {
352: // Create an "identity" transformer - copies input to output
353: t = TransformerFactory.newInstance().newTransformer();
354: }
355:
356: // Set trasformation output properties
357:
358: // DTD
359: if (this .doctypePublic != null) {
360: t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
361: this .doctypePublic);
362: }
363: if (this .doctypeSystem != null) {
364: t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
365: this .doctypeSystem);
366: }
367:
368: // set XSLT output properties
369: if (this .xsltOutputProperties != null) {
370: t.setOutputProperties(this .xsltOutputProperties);
371: }
372:
373: // Output encoding
374: String preferredEncoding = t.getOutputProperties().getProperty(
375: OutputKeys.ENCODING);
376: if (preferredEncoding != null) {
377: response.setCharacterEncoding(preferredEncoding);
378: }
379:
380: // Create the trasformation source using our custom ObjectInputSource
381: InputSource inputSource = new ObjectInputSource(object);
382: Source source = new SAXSource(saxReader, inputSource);
383:
384: // Create the trasformation result
385: // Result result = new StreamResult(response.getWriter());
386: Result result = new StreamResult(response.getOutputStream());
387:
388: // Go!
389: t.transform(source, result);
390:
391: }
392:
393: private static Properties stringToProperties(String string)
394: throws IOException {
395: Properties properties = new Properties();
396: InputStream inputStream = new ByteArrayInputStream(string
397: .getBytes());
398: try {
399: properties.load(inputStream);
400: } finally {
401: inputStream.close();
402: }
403: return properties;
404: }
405:
406: /**
407: * This SAX XMLReader generates an XML document from a JDBC Resultset.
408: */
409: final class ResultSetXMLReader extends ObjectXMLReader {
410:
411: /**
412: * @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
413: */
414: public void parse(ObjectInputSource input) throws IOException,
415: SAXException {
416:
417: try {
418:
419: // SimpleDateFormat objects are lazily instantiated
420: // Note that SimpleDateFormat objects cannot be used
421: // concurrently by multiple threads
422: SimpleDateFormat timestampFormatter = null;
423: SimpleDateFormat dateFormatter = null;
424: SimpleDateFormat timeFormatter = null;
425:
426: ResultSet resultSet = (ResultSet) input.getObject();
427:
428: this .handler.startDocument();
429: this .handler.startElement(
430: ContentHandlerUtils.DUMMY_NSU,
431: XMLServlet.this .rootElementName,
432: XMLServlet.this .rootElementName,
433: ContentHandlerUtils.DUMMY_ATTS);
434:
435: final ResultSetMetaData rsmd = resultSet.getMetaData();
436:
437: // Get an array of column names
438: // It is user responsibility to ensure column names are valid
439: // XML element names
440: final String[] columns = new String[rsmd
441: .getColumnCount()];
442: for (int i = 0; i < columns.length; i++) {
443: // The set of column names begins with '1' rather than '0'
444: String columnName = rsmd.getColumnName(i + 1);
445: if (XMLServlet.this .columnCase == CASE_LOWER) {
446: columns[i] = columnName.toLowerCase();
447: } else if (XMLServlet.this .columnCase == CASE_ORIGINAL) {
448: columns[i] = columnName;
449: } else if (XMLServlet.this .columnCase == CASE_UPPER) {
450: columns[i] = columnName.toUpperCase();
451: } else {
452: throw new SAXException(
453: "Can't happen! Invalid column-case: "
454: + XMLServlet.this .columnCase);
455: }
456: }
457:
458: // Get an array of the types of each column
459: final int[] columnTypes = new int[columns.length];
460: for (int i = 0; i < columnTypes.length; i++) {
461: // The set of column types also begins with '1' rather than
462: // '0'
463: columnTypes[i] = rsmd.getColumnType(i + 1);
464: }
465:
466: do {
467:
468: // if the user specified an empty row element name, just
469: // omit the element.
470: if (XMLServlet.this .rowElementName.length() > 0) {
471: this .handler.startElement(
472: ContentHandlerUtils.DUMMY_NSU,
473: XMLServlet.this .rowElementName,
474: XMLServlet.this .rowElementName,
475: ContentHandlerUtils.DUMMY_ATTS);
476: }
477:
478: for (int i = 0; i < columns.length; i++) {
479: switch (columnTypes[i]) {
480:
481: case Types.DATE:
482: // format date only
483: java.sql.Date date = resultSet
484: .getDate(i + 1);
485: if (date != null) {
486: if (dateFormatter == null) {
487: dateFormatter = new SimpleDateFormat(
488: PATTERN_DATE);
489: }
490: String d = dateFormatter.format(date);
491: ContentHandlerUtils.tag(this .handler,
492: columns[i], d);
493: }
494: break;
495:
496: case Types.TIME:
497: // format time only
498: java.sql.Time time = resultSet
499: .getTime(i + 1);
500: if (time != null) {
501: if (timeFormatter == null) {
502: timeFormatter = new SimpleDateFormat(
503: PATTERN_TIME);
504: }
505: String d = timeFormatter.format(time);
506: ContentHandlerUtils.tag(this .handler,
507: columns[i], d);
508: }
509: break;
510:
511: case Types.TIMESTAMP:
512: Timestamp timestamp = resultSet
513: .getTimestamp(i + 1);
514: if (timestamp != null) {
515: if (timestampFormatter == null) {
516: timestampFormatter = new SimpleDateFormat(
517: PATTERN_TIMESTAMP);
518: }
519: String d = timestampFormatter
520: .format(timestamp);
521: ContentHandlerUtils.tag(this .handler,
522: columns[i], d);
523: }
524: break;
525:
526: case Types.VARCHAR:
527: case Types.CHAR:
528: case Types.LONGVARCHAR:
529: Reader reader = resultSet
530: .getCharacterStream(i + 1);
531: readerToXml(reader, columns[i]);
532: break;
533:
534: case Types.CLOB:
535: Clob clob = resultSet.getClob(i + 1);
536: if (clob != null) {
537: Reader clobReader = clob
538: .getCharacterStream();
539: readerToXml(clobReader, columns[i]);
540: }
541: break;
542:
543: // TODO base64 BLOBs?
544:
545: default:
546: Object object = resultSet.getObject(i + 1);
547: if (object != null) {
548: ContentHandlerUtils.tag(this .handler,
549: columns[i], object.toString());
550: }
551:
552: }
553: }
554:
555: if (XMLServlet.this .rowElementName.length() > 0) {
556: this .handler.endElement(
557: ContentHandlerUtils.DUMMY_NSU,
558: XMLServlet.this .rowElementName,
559: XMLServlet.this .rowElementName);
560: }
561:
562: } while (resultSet.next());
563:
564: this .handler.endElement(ContentHandlerUtils.DUMMY_NSU,
565: XMLServlet.this .rootElementName,
566: XMLServlet.this .rootElementName);
567: this .handler.endDocument();
568:
569: } catch (SQLException sqle) {
570: throw new SAXException("SQL Error.", sqle);
571: }
572: }
573:
574: /**
575: * Consume a Reader and generate SAX events.
576: *
577: * @param reader
578: * A Reader instance, maybe null.
579: * @param columnName
580: * The name of the table column.
581: */
582: private void readerToXml(Reader reader, String columnName)
583: throws SAXException, IOException {
584: if (reader != null) {
585: try {
586: // wrap our reader in order to strip invalid XML chars
587: reader = new XMLTextReader(reader);
588: this .handler.startElement(
589: ContentHandlerUtils.DUMMY_NSU, columnName,
590: columnName, ContentHandlerUtils.DUMMY_ATTS);
591:
592: char[] buffer = new char[4096];
593: int length;
594: while ((length = reader.read(buffer)) >= 0) {
595: this .handler.characters(buffer, 0, length);
596: }
597: this .handler.endElement(
598: ContentHandlerUtils.DUMMY_NSU, columnName,
599: columnName);
600: } finally {
601: reader.close();
602: }
603: }
604: }
605:
606: }
607:
608: /**
609: * This SAX XMLReader generates an empty XML document. This is used for
610: * generating a dummy default document.
611: */
612: final class EmptyXMLReader extends ObjectXMLReader {
613:
614: /**
615: * @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
616: */
617: public void parse(ObjectInputSource input) throws IOException,
618: SAXException {
619: this.handler.startDocument();
620: this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
621: XMLServlet.this.rootElementName,
622: XMLServlet.this.rootElementName,
623: ContentHandlerUtils.DUMMY_ATTS);
624: this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
625: XMLServlet.this.rootElementName,
626: XMLServlet.this.rootElementName);
627: this.handler.endDocument();
628: }
629:
630: }
631: }
|