package org.gomba;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Perform a write operation on a JDBC data store. the SQL in the
* <code>query</code> init-param can be an INSERT, UPDATE or DELETE statement.
* This servlet inherits the init-params of {@link org.gomba.AbstractServlet},
* plus:
* <dl>
* <dt>query</dt>
* <dd>The SQL update 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>batch</dt>
* <dd>true if the SQL contains multiple statements separated by semicolons.
* Muliple updates are performed as part of single transaction. (Optional)</dd>
* <dt>http-method</dt>
* <dd>The value can be POST, PUT or DELETE. (Required)</dd>
* </dl>
*
* Note about HTTP method usage. The POST method is normally used for creation
* (INSERT in SQL) operations. The PUT method is normally used for update
* (UPDATE in SQL) operations. The DELETE method is obviously used for deletion
* (DELETE in SQL) operations.
*
* @author Flavio Tordini
* @version $Id: UpdateServlet.java,v 1.12 2005/10/19 13:00:21 flaviotordini Exp $
*/
public class UpdateServlet extends AbstractServlet {
private final static String INIT_PARAM_HTTP_METHOD = "http-method";
private final static String INIT_PARAM_QUERY = "query";
private final static String INIT_PARAM_BATCH = "batch";
/**
* <code>true</code> if this servlet supports the POST HTTP method.
*/
private boolean supportPost;
/**
* <code>true</code> if this servlet supports the PUT HTTP method.
*/
private boolean supportPut;
/**
* <code>true</code> if this servlet supports the DELETE HTTP method.
*/
private boolean supportDelete;
/**
* The parsed query definitions. It is null when the query is dynamic, i.e.
* a dynamic resource (a JSP) is used to generate the SQL. List of
* QueryDefinition.
*/
private List queryDefinitions;
/**
* The path of a resource that dynamically generates a SQL query.
*/
private String queryResource;
private boolean batch;
/**
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// supported HTTP method
String httpMethod = config.getInitParameter(INIT_PARAM_HTTP_METHOD);
if (httpMethod == null) {
throw new ServletException("Missing init-param: "
+ INIT_PARAM_HTTP_METHOD);
}
if (httpMethod.equals("POST")) {
this.supportPost = true;
} else if (httpMethod.equals("PUT")) {
this.supportPut = true;
} else if (httpMethod.equals("DELETE")) {
this.supportDelete = true;
} else {
throw new ServletException("Unsupported HTTP method: " + httpMethod);
}
// is batch?
String batchStr = config.getInitParameter(INIT_PARAM_BATCH);
this.batch = Boolean.valueOf(batchStr).booleanValue();
// parse the query definition(s)
try {
String query = config.getInitParameter(INIT_PARAM_QUERY);
if (!query.startsWith("/")) {
this.queryDefinitions = parseQueryDefinitions(query);
} else {
this.queryResource = query;
}
} catch (Exception e) {
throw new ServletException("Error parsing query definition(s).", e);
}
}
private List parseBatchQueryDefinitions(String query) throws Exception {
StringTokenizer st = new StringTokenizer(query, ";");
List queryDefinitions = new ArrayList(st.countTokens());
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
if (token.length() == 0) {
continue;
}
QueryDefinition queryDefinition = new QueryDefinition(token);
queryDefinitions.add(queryDefinition);
}
return queryDefinitions;
}
private List parseQueryDefinitions(String query) throws Exception {
List queryDefinitions;
if (this.batch) {
queryDefinitions = parseBatchQueryDefinitions(query);
} else {
queryDefinitions = Collections.singletonList(new QueryDefinition(
query));
}
return queryDefinitions;
}
/**
* @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected void doDelete(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if (this.supportDelete) {
processRequest(request, response);
} else {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
/**
* @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if (this.supportPost) {
processRequest(request, response);
} else {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
/**
* @see javax.servlet.http.HttpServlet#doPut(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected void doPut(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if (this.supportPut) {
processRequest(request, response);
} else {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
/**
* Get the QueryDefinition, it can be a fixed QueryDefinition created at
* init-time. Or a dynamic one created by evaluating a JSP.
*/
private List getQueryDefinitions(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
List requestQueryDefinitions;
if (this.queryDefinitions == null) {
// dynamic query
String sql = getDynamicQuery(this.queryResource, request, response);
try {
requestQueryDefinitions = parseQueryDefinitions(sql);
} catch (Exception e) {
throw new ServletException(
"Error parsing query definition(s).", e);
}
} else {
// fixed query
requestQueryDefinitions = this.queryDefinitions;
}
return requestQueryDefinitions;
}
/**
* The real work is done here.
*
* @param request
* The HTTP request
* @param response
* The HTTP response
*/
protected final void processRequest(HttpServletRequest request,
HttpServletResponse response) 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(s)
List requestQueryDefinitions = getQueryDefinitions(request, response);
if (requestQueryDefinitions.isEmpty()) {
throw new ServletException("Missing query definitions.");
}
// find out if this request is part of a transaction
Transaction transaction = getTransaction(parameterResolver);
Connection connection = null;
Query.QueryResult queryResult = null;
// surround everything in this try/finally to be able to free JDBC
// resources even in case of exceptions
try {
// get a JDBC connection
try {
if (transaction == null) {
connection = getDataSource().getConnection();
if (this.batch) {
connection.setAutoCommit(false);
}
} else {
if (isDebugMode()) {
log("Request is part of transaction: "
+ transaction.getUri());
}
connection = transaction.getConnection();
}
} catch (SQLException e) {
throw new ServletException("Error getting JDBC connection.", e);
}
// loop through query definitions
for (Iterator qdIterator = requestQueryDefinitions.iterator(); qdIterator
.hasNext();) {
QueryDefinition requestQueryDefinition = (QueryDefinition) qdIterator
.next();
// build the Query
final Query query = getQuery(requestQueryDefinition,
parameterResolver);
// execute the query
Query.QueryResult newQueryResult = query.execute(connection);
if (newQueryResult != null) {
// now that we've (maybe) used the data from the previous query,
// let's free the ResultSet
if (queryResult != null) {
try {
queryResult.close();
} catch (Exception e) {
throw new ServletException(
"Error freeing JDBC resources.", e);
}
}
queryResult = newQueryResult;
ResultSet resultSet = queryResult.getResultSet();
maybeMoveCursor(resultSet);
parameterResolver.setResultSet(resultSet);
}
}
// set the response headers
setResponseHeaders(response, parameterResolver);
// set the HTTP status code
int httpStatus = getHttpStatusCode();
if (httpStatus != HttpServletResponse.SC_OK) {
response.setStatus(httpStatus);
}
if (transaction == null && this.batch) {
connection.commit();
}
} catch (Exception e) {
if (transaction == null && connection != null && this.batch) {
try {
connection.rollback();
} catch (Exception e2) {
log("Error rolling back JDBC connection.", e2);
}
}
if (e instanceof ServletException) {
throw (ServletException) e;
}
if (e instanceof IOException) {
throw (IOException) e;
}
throw new ServletException("Error processing request.", e);
} finally {
// *always* free the JDBC resources!
if (queryResult != null) {
try {
queryResult.close();
} catch (Exception e) {
log("Error freeing JDBC resources.", e);
}
}
// close the JDBC connection if this request is not part of a
// transaction
if (transaction == null && connection != null) {
try {
if (this.batch) {
connection.setAutoCommit(true);
}
connection.close();
} catch (Exception e) {
throw new ServletException(
"Error closing JDBC connection.", e);
}
}
// processing time
if (isDebugMode()) {
log(getProfilingMessage(request, startTime));
}
}
}
}