package org.gomba;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.sql.Clob;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.gomba.utils.xml.XMLTextReader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Render data accessed via JDBC in XML syntax. The SQL in the
* <code>query</code> init-param should be a SELECT and must return a
* resultset. This servlet inherits the init-params of
* {@link org.gomba.AbstractServlet}, plus:
*
* <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>root-element</dt>
* <dd>The output XML root element name. Defaults to "resultSet" (Optional)
* </dd>
*
* <dt>row-element</dt>
* <dd>The output XML row element name. If an empty value is specified the row
* element is omitted. This is useful for queries that always return a single
* row. Defaults to "row" (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>
*
* <dt>xslt-output-properties</dt>
* <dd>XSLT output properties in Java Properties format.
* http://www.w3.org/TR/xslt#output (Optional)</dd>
*
* <dt>column-case</dt>
* <dd>The case of XML element names. May be 'lower', 'upper' or 'original'.
* Default: 'lower' (Optional)</dd>
*
* </dl>
*
* <p>
* This servlet can handle the following HTTP methods: GET, HEAD.
* </p>
*
* <p>
* The row element children are named after the corresponding JDBC column. If a
* column name in your db is not a valid XML element name or you would like to
* use a different name in the XML, just use the <code>AS</code> SQL keyword
* to change it. It is the user responsibility to create a DTD or XML Schema for
* the generated XML.
* </p>
*
* @author Flavio Tordini
* @version $Id: XMLServlet.java,v 1.15 2005/12/03 15:47:11 flaviotordini Exp $
*/
public class XMLServlet extends SingleQueryServlet {
private final static String ELEMENT_ROOT = "resultSet";
private final static String ELEMENT_ROW = "row";
private static final String PATTERN_TIMESTAMP = "yyyy-MM-dd'T'HH:mm:ss";
private static final String PATTERN_DATE = "yyyy-MM-dd";
private static final String PATTERN_TIME = "HH:mm:ss";
private static final int CASE_ORIGINAL = 1;
private static final int CASE_UPPER = 2;
private static final int CASE_LOWER = 3;
/**
* 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;
/**
* The element names.
*/
private String rootElementName, rowElementName;
/**
* The case of the resultset colums.
*/
private int columnCase;
/**
* XSLT parameters.
*/
private Map xsltFixedParameters;
/**
* XSLT output properties.
*/
private Properties xsltOutputProperties;
/**
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// DTD
this.doctypePublic = config.getInitParameter("doctype-public");
this.doctypeSystem = config.getInitParameter("doctype-system");
// MIME
this.mediaType = config.getInitParameter("media-type");
// column case
String ccase = config.getInitParameter("column-case");
if (ccase == null) {
// default
this.columnCase = CASE_LOWER;
} else if (ccase.equals("lower")) {
this.columnCase = CASE_LOWER;
} else if (ccase.equals("upper")) {
this.columnCase = CASE_UPPER;
} else if (ccase.equals("original")) {
this.columnCase = CASE_ORIGINAL;
} else {
throw new ServletException("Invalid 'column-case' value: " + ccase);
}
// element names
this.rootElementName = config.getInitParameter("root-element");
if (this.rootElementName == null) {
this.rootElementName = ELEMENT_ROOT;
}
this.rowElementName = config.getInitParameter("row-element");
if (this.rowElementName == null) {
this.rowElementName = ELEMENT_ROW;
}
// XSLT
final String xsltStyleSheet = config.getInitParameter("xslt");
if (xsltStyleSheet != null) {
// Create a templates object, which is the processed,
// thread-safe representation of the stylesheet.
InputStream is = getServletContext().getResourceAsStream(
xsltStyleSheet);
if (is == null) {
throw new ServletException("Cannot find stylesheet: "
+ xsltStyleSheet);
}
try {
TransformerFactory tfactory = TransformerFactory.newInstance();
Source xslSource = new StreamSource(is);
// 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 = stringToProperties(xsltParams);
} catch (Exception e) {
throw new ServletException("Error parsing XSLT params: "
+ xsltParams, e);
}
}
// create a map of xslt output properties
final String xsltOutputProperties = config
.getInitParameter("xslt-output-properties");
if (xsltOutputProperties != null) {
try {
this.xsltOutputProperties = stringToProperties(xsltOutputProperties);
} catch (Exception e) {
throw new ServletException(
"Error parsing XSLT output properties: "
+ xsltOutputProperties, e);
}
}
}
}
/**
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected final void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processRequest(request, response, true);
}
/**
* @see javax.servlet.http.HttpServlet#doHead(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected final void doHead(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processRequest(request, response, false);
}
/**
* @see org.gomba.AbstractServlet#doOutput(java.sql.ResultSet,
* javax.servlet.http.HttpServletResponse, ParameterResolver)
*/
protected void doOutput(ResultSet resultSet, HttpServletResponse response,
ParameterResolver parameterResolver) throws Exception {
// Create the sax "parser".
ObjectXMLReader saxReader = new ResultSetXMLReader();
// generate XML and write it to the response body
serializeXML(resultSet, saxReader, response);
}
/**
* @see org.gomba.AbstractServlet#doDefaultOutput(javax.servlet.http.HttpServletResponse)
*/
protected void doDefaultOutput(HttpServletResponse response)
throws Exception {
// Create a dummy sax "parser".
ObjectXMLReader saxReader = new EmptyXMLReader();
// generate XML and write it to the response body
serializeXML(null, saxReader, response);
}
/**
* Serialize an object to XML using SAX and TrAX APIs in a smart way.
*
* @param object
* The object to serialize
* @param saxReader
* The SAX "parser"
* @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(Object object, ObjectXMLReader saxReader,
HttpServletResponse response) throws Exception {
// Let the HTTP client know the output content type
String mediaType = null;
if (this.mediaType != null) {
mediaType = this.mediaType;
}
if (mediaType == null && this.xsltOutputProperties != null) {
mediaType = this.xsltOutputProperties
.getProperty(OutputKeys.MEDIA_TYPE);
}
if (mediaType == null && this.templates != null) {
mediaType = this.templates.getOutputProperties().getProperty(
OutputKeys.MEDIA_TYPE);
}
if (mediaType == null) {
mediaType = "text/xml";
}
response.setContentType(mediaType);
// 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
// DTD
if (this.doctypePublic != null) {
t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, this.doctypePublic);
}
if (this.doctypeSystem != null) {
t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, this.doctypeSystem);
}
// set XSLT output properties
if (this.xsltOutputProperties != null) {
t.setOutputProperties(this.xsltOutputProperties);
}
// Output encoding
String preferredEncoding = t.getOutputProperties().getProperty(
OutputKeys.ENCODING);
if (preferredEncoding != null) {
response.setCharacterEncoding(preferredEncoding);
}
// Create the trasformation source using our custom ObjectInputSource
InputSource inputSource = new ObjectInputSource(object);
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);
}
private static Properties stringToProperties(String string)
throws IOException {
Properties properties = new Properties();
InputStream inputStream = new ByteArrayInputStream(string.getBytes());
try {
properties.load(inputStream);
} finally {
inputStream.close();
}
return properties;
}
/**
* This SAX XMLReader generates an XML document from a JDBC Resultset.
*/
final class ResultSetXMLReader extends ObjectXMLReader {
/**
* @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
*/
public void parse(ObjectInputSource input) throws IOException,
SAXException {
try {
// SimpleDateFormat objects are lazily instantiated
// Note that SimpleDateFormat objects cannot be used
// concurrently by multiple threads
SimpleDateFormat timestampFormatter = null;
SimpleDateFormat dateFormatter = null;
SimpleDateFormat timeFormatter = null;
ResultSet resultSet = (ResultSet) input.getObject();
this.handler.startDocument();
this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
XMLServlet.this.rootElementName,
XMLServlet.this.rootElementName,
ContentHandlerUtils.DUMMY_ATTS);
final ResultSetMetaData rsmd = resultSet.getMetaData();
// Get an array of column names
// It is user responsibility to ensure column names are valid
// XML element names
final String[] columns = new String[rsmd.getColumnCount()];
for (int i = 0; i < columns.length; i++) {
// The set of column names begins with '1' rather than '0'
String columnName = rsmd.getColumnName(i + 1);
if (XMLServlet.this.columnCase == CASE_LOWER) {
columns[i] = columnName.toLowerCase();
} else if (XMLServlet.this.columnCase == CASE_ORIGINAL) {
columns[i] = columnName;
} else if (XMLServlet.this.columnCase == CASE_UPPER) {
columns[i] = columnName.toUpperCase();
} else {
throw new SAXException(
"Can't happen! Invalid column-case: "
+ XMLServlet.this.columnCase);
}
}
// Get an array of the types of each column
final int[] columnTypes = new int[columns.length];
for (int i = 0; i < columnTypes.length; i++) {
// The set of column types also begins with '1' rather than
// '0'
columnTypes[i] = rsmd.getColumnType(i + 1);
}
do {
// if the user specified an empty row element name, just
// omit the element.
if (XMLServlet.this.rowElementName.length() > 0) {
this.handler.startElement(
ContentHandlerUtils.DUMMY_NSU,
XMLServlet.this.rowElementName,
XMLServlet.this.rowElementName,
ContentHandlerUtils.DUMMY_ATTS);
}
for (int i = 0; i < columns.length; i++) {
switch (columnTypes[i]) {
case Types.DATE:
// format date only
java.sql.Date date = resultSet.getDate(i + 1);
if (date != null) {
if (dateFormatter == null) {
dateFormatter = new SimpleDateFormat(
PATTERN_DATE);
}
String d = dateFormatter.format(date);
ContentHandlerUtils.tag(this.handler,
columns[i], d);
}
break;
case Types.TIME:
// format time only
java.sql.Time time = resultSet.getTime(i + 1);
if (time != null) {
if (timeFormatter == null) {
timeFormatter = new SimpleDateFormat(
PATTERN_TIME);
}
String d = timeFormatter.format(time);
ContentHandlerUtils.tag(this.handler,
columns[i], d);
}
break;
case Types.TIMESTAMP:
Timestamp timestamp = resultSet.getTimestamp(i + 1);
if (timestamp != null) {
if (timestampFormatter == null) {
timestampFormatter = new SimpleDateFormat(
PATTERN_TIMESTAMP);
}
String d = timestampFormatter.format(timestamp);
ContentHandlerUtils.tag(this.handler,
columns[i], d);
}
break;
case Types.VARCHAR:
case Types.CHAR:
case Types.LONGVARCHAR:
Reader reader = resultSet.getCharacterStream(i + 1);
readerToXml(reader, columns[i]);
break;
case Types.CLOB:
Clob clob = resultSet.getClob(i + 1);
if (clob != null) {
Reader clobReader = clob.getCharacterStream();
readerToXml(clobReader, columns[i]);
}
break;
// TODO base64 BLOBs?
default:
Object object = resultSet.getObject(i + 1);
if (object != null) {
ContentHandlerUtils.tag(this.handler,
columns[i], object.toString());
}
}
}
if (XMLServlet.this.rowElementName.length() > 0) {
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
XMLServlet.this.rowElementName,
XMLServlet.this.rowElementName);
}
} while (resultSet.next());
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
XMLServlet.this.rootElementName,
XMLServlet.this.rootElementName);
this.handler.endDocument();
} catch (SQLException sqle) {
throw new SAXException("SQL Error.", sqle);
}
}
/**
* Consume a Reader and generate SAX events.
*
* @param reader
* A Reader instance, maybe null.
* @param columnName
* The name of the table column.
*/
private void readerToXml(Reader reader, String columnName)
throws SAXException, IOException {
if (reader != null) {
try {
// wrap our reader in order to strip invalid XML chars
reader = new XMLTextReader(reader);
this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
columnName, columnName,
ContentHandlerUtils.DUMMY_ATTS);
char[] buffer = new char[4096];
int length;
while ((length = reader.read(buffer)) >= 0) {
this.handler.characters(buffer, 0, length);
}
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
columnName, columnName);
} finally {
reader.close();
}
}
}
}
/**
* This SAX XMLReader generates an empty XML document. This is used for
* generating a dummy default document.
*/
final class EmptyXMLReader extends ObjectXMLReader {
/**
* @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
*/
public void parse(ObjectInputSource input) throws IOException,
SAXException {
this.handler.startDocument();
this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
XMLServlet.this.rootElementName,
XMLServlet.this.rootElementName,
ContentHandlerUtils.DUMMY_ATTS);
this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
XMLServlet.this.rootElementName,
XMLServlet.this.rootElementName);
this.handler.endDocument();
}
}
}