package org.gomba;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Base class for servlets that perform a SELECT and render the results to the
* HTTP response body.
*
* </p>
* Init params:
* <dl>
* <dt>query</dt>
* <dd>The SQL query to execute. May contain ${} parameters. This init-param
* also accepts a path to a dynamic resource (a JSP) when dynamic SQL generation
* is needed. The path must begin with a "/" and is interpreted as relative to
* the current context root. (Required)</dd>
* <dt>skip</dt>
* <dd>The number of records to skip. May contain ${} parameters. (Optional)
* </dd>
* <dt>max</dt>
* <dd>The maximum number of records to load. May contain ${} parameters.
* (Optional)</dd>
* <dt>nodata-http-status</dt>
* <dd>The HTTP status code in case of empty resultset. If the code is 200 (OK)
* then the subclassing servlet will output its default value. Defaults to 200
* (OK). A useful code is 404 (Not found). (Optional)</dd>
* <dt>nodata-default-resource</dt>
* <dd>Path to a resource to serve in case of empty resultset. The path must
* begin with a "/" and is interpreted as relative to the current context root.
* When this init-param is not specified, the subclassing servlet default output
* is used. (Optional)</dd>
* </dl>
* </p>
*
* @author Flavio Tordini
* @version $Id: SingleQueryServlet.java,v 1.5 2005/10/19 13:48:16 flaviotordini Exp $
*/
public abstract class SingleQueryServlet extends AbstractServlet {
private final static String INIT_PARAM_QUERY = "query";
private final static String INIT_PARAM_SKIP = "skip";
private final static String INIT_PARAM_MAX = "max";
private final static String INIT_PARAM_NO_DATA_HTTP_STATUS = "nodata-http-status";
private final static String INIT_PARAM_NO_DATA_DEFAULT_RESOURCE = "nodata-default-resource";
/**
* The parsed query definition. It is null when the query is dynamic, i.e. a
* dynamic resource (a JSP) is used to generate the SQL.
*/
private QueryDefinition queryDefinition;
/**
* The path of a resource that dynamically generates a SQL query.
*/
private String queryResource;
/**
* Skip and max expressions. These are only set when a dynamic query is
* used.
*/
private Expression skip, max;
/**
* The HTTP status code in case of empty resultset.
*/
private int noDataHttpStatusCode = HttpServletResponse.SC_OK;
/**
* Path to a resource to serve in case of empty resultset. If null the
* doDefaultOutput method is invoked.
*/
private String noDataDefaultResource;
/**
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// parse the query definition
try {
String query = config.getInitParameter(INIT_PARAM_QUERY);
String skip = config.getInitParameter(INIT_PARAM_SKIP);
String max = config.getInitParameter(INIT_PARAM_MAX);
if (!query.startsWith("/")) {
this.queryDefinition = new QueryDefinition(query, skip, max);
} else {
this.queryResource = query;
if (skip != null) {
this.skip = new Expression(skip);
}
if (max != null) {
this.max = new Expression(max);
}
}
} catch (Exception e) {
throw new ServletException("Error parsing query definition.", e);
}
// the HTTP status code to send in case of empty resultset.
try {
String noDataHttpStatusCodeString = config
.getInitParameter(INIT_PARAM_NO_DATA_HTTP_STATUS);
if (noDataHttpStatusCodeString != null) {
this.noDataHttpStatusCode = Integer
.parseInt(noDataHttpStatusCodeString);
}
} catch (NumberFormatException e) {
throw new ServletException("Error parsing "
+ INIT_PARAM_NO_DATA_HTTP_STATUS, e);
}
// default resource in case of empty resultset.
this.noDataDefaultResource = config
.getInitParameter(INIT_PARAM_NO_DATA_DEFAULT_RESOURCE);
}
/**
* Get the QueryDefinition, it can be a fixed QueryDefinition created at
* init-time. Or a dynamic one created by evaluating a JSP.
*/
private QueryDefinition getQueryDefinition(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
QueryDefinition requestQueryDefinition;
if (this.queryDefinition == null) {
// dynamic query
String sql = getDynamicQuery(this.queryResource, request, response);
try {
requestQueryDefinition = new QueryDefinition(sql, this.skip,
this.max);
} catch (Exception e) {
throw new ServletException("Error parsing query definition.", e);
}
} else {
// fixed query
requestQueryDefinition = this.queryDefinition;
}
return requestQueryDefinition;
}
/**
* The real work is done here.
*
* @param request
* The HTTP request
* @param response
* The HTTP response
* @param renderBody
* Wheter to render the response body or not.
*/
protected final void processRequest(HttpServletRequest request,
HttpServletResponse response, boolean renderBody)
throws ServletException, IOException {
// get current time for benchmarking purposes
final long startTime = System.currentTimeMillis();
// create the parameter resolver that will help us throughout this
// request
final ParameterResolver parameterResolver = new ParameterResolver(
request);
// get the query definition
QueryDefinition requestQueryDefinition = getQueryDefinition(request,
response);
// build the Query
final Query query = getQuery(requestQueryDefinition, parameterResolver);
// find out if this request is part of a transaction
Transaction transaction = getTransaction(parameterResolver);
Query.QueryResult queryResult = null;
Connection connection = null;
// surround everything in this try/finally to be able to free JDBC
// resources even in case of exceptions
try {
try {
if (transaction == null) {
connection = getDataSource().getConnection();
} else {
if (isDebugMode()) {
log("Request is part of transaction: "
+ transaction.getUri());
}
connection = transaction.getConnection();
}
// execute the query
queryResult = query.execute(connection);
// queryResult may be null, if the query is an update.
if (queryResult != null) {
// Make sure the resultset cursor is positioned on a row. If
// resultset is empty or after the last row, stop processing
// this request.
if (!maybeMoveCursor(queryResult.getResultSet())) {
// the resultset is empty!
// if status is not 200 set the HTTP status and stop
if (this.noDataHttpStatusCode != HttpServletResponse.SC_OK) {
response.sendError(this.noDataHttpStatusCode);
return;
}
// set the response headers right away
// if the response headers include an expression using
// the 'column' domain (which requires a resultset
// available to the ParameterResolver) an exception will
// be thrown. The exception is caught and logged if in
// debug mode.
Map responseHeaders = getResponseHeaders();
if (responseHeaders != null) {
try {
for (Iterator i = responseHeaders.entrySet()
.iterator(); i.hasNext();) {
Map.Entry mapEntry = (Map.Entry) i.next();
String headerName = (String) mapEntry
.getKey();
Object headerValue;
try {
headerValue = ((Expression) mapEntry
.getValue())
.replaceParameters(parameterResolver);
} catch (ParameterResolver.UnavailableResultSetException urse) {
if (isDebugMode()) {
log("Cannot set response header: "
+ headerName, urse);
}
continue;
}
setResponseHeader(response, headerName,
headerValue);
}
} catch (Exception e) {
throw new ServletException(
"Error setting response headers.", e);
}
}
if (this.noDataDefaultResource == null) {
// default output
try {
// subclasses will implement this!
doDefaultOutput(response);
} catch (Exception e) {
throw new ServletException(
"Error rendering default output.", e);
}
} else {
// default resource
serveDefaultResource(this.noDataDefaultResource,
request, response);
}
// stop processing this request.
return;
}
// set reference to the result set in order to resolve
// 'column' domain parameters
parameterResolver.setResultSet(queryResult.getResultSet());
}
} catch (Exception e) {
// log the SQL for debugging
log("Error executing query: " + query, e);
// but don't expose the SQL nor the exception on the web for
// security reasons. We don't want users to be able to see our
// SQL nor the JDBC driver exception messages which usually
// contain table names and such.
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Error executing query. See logs for details.");
return;
}
try {
ResultSet resultSet = null;
if (queryResult != null) {
resultSet = queryResult.getResultSet();
}
// subclasses will implement this!
doInput(resultSet, request, parameterResolver, connection);
} catch (Exception e) {
throw new ServletException("Error processing request body.", e);
}
// set the response headers
setResponseHeaders(response, parameterResolver);
// set the HTTP status code
if (getHttpStatusCode() != HttpServletResponse.SC_OK) {
response.setStatus(getHttpStatusCode());
}
// optionally write to the response body
if (renderBody) {
if (queryResult == null) {
throw new ServletException(
"Resultset is null. The query didn't return a resultset.");
}
try {
// subclasses will implement this!
doOutput(queryResult.getResultSet(), response,
parameterResolver);
} catch (Exception e) {
throw new ServletException("Error rendering results.", e);
}
}
} finally {
// *always* free the JDBC resources!
try {
if (queryResult != null) {
try {
queryResult.close();
} catch (Exception e) {
throw new ServletException(
"Error freeing JDBC resources.", e);
}
}
} finally {
// close the JDBC connection if this request is not part of a
// transaction
if (transaction == null && connection != null) {
try {
connection.close();
} catch (Exception e) {
throw new ServletException(
"Error closing JDBC connection.", e);
}
}
}
// processing time
if (isDebugMode()) {
log(getProfilingMessage(request, startTime));
}
}
}
/**
* Override this method in order to process data from the request body. The
* contract for subclasses is not to close the ResultSet and not to call
* ResultSet.next().
*
* @param resultSet
* The resultset, may be null.
* @param request
* The HTTP request to read from
* @param parameterResolver
* The object used to resolve parameters.
*/
protected void doInput(ResultSet resultSet,
HttpServletRequest request, ParameterResolver parameterResolver, Connection connection)
throws Exception {
// dummy
}
/**
* Render the content of the resultset in the response body. The contract
* for subclasses is not to close the ResultSet and expect it to be
* positioned on the first row to render (This means ResultSet.next() should
* be called <strong>after </strong> the first row has been rendered.
*
* @param resultSet
* The resultset to render
* @param response
* The HTTP response to write to
* @param parameterResolver
* The object used to resolve parameters.
*/
protected void doOutput(ResultSet resultSet,
HttpServletResponse response, ParameterResolver parameterResolver)
throws Exception {
// dummy
}
/**
* Render a default value when the resultset is empty (0 rows).
*
* @param response
* The HTTP response to write to
*/
protected void doDefaultOutput(HttpServletResponse response)
throws Exception {
// dummy
}
/**
* Serve a default resource.
*
* @param defaultResource
* Path to the resource
* @param request
* The HTTP request
* @param response
* The HTTP response
*/
private final void serveDefaultResource(String defaultResource,
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// get the dispatcher
RequestDispatcher dispatcher = getServletContext()
.getRequestDispatcher(defaultResource);
if (dispatcher == null) {
throw new ServletException(
"Cannot get a RequestDispatcher for path: "
+ defaultResource);
}
dispatcher.forward(request, response);
}
}