package org.gomba;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.gomba.utils.servlet.ServletParameterUtils;
/**
* This servlets dispatches the same request to mutiple servlets overriding the
* original HTTP method and capturing the output of each servlet into the
* request scope. The output of the last operation is sent directly the client.
*
* <p>
* This servlet inherits the init-params of
* {@link org.gomba.TransactorAbstractServlet}
* </p>
*
* <p>
* Init-params:
* <dl>
* <dt>http-method</dt>
* <dd>The value can be GET, POST, PUT or DELETE. (Required)</dd>
* <dt>operations</dt>
* <dd>A list of operations to execute, one per line. Syntax:
* <code>HTTPMethod ResourceNameOrPath [RequestAttributeName]</code>.
* Example: <code>GET myServlet</code>. (Required)</dd>
* </dl>
* </p>
*
* @author Flavio Tordini
* @version $Id: MultiServlet.java,v 1.1 2004/11/26 17:52:58 flaviotordini Exp $
*/
public class MultiServlet extends TransactorAbstractServlet {
/**
* <code>true</code> if this servlet supports the GET HTTP method.
*/
private boolean supportGet;
/**
* <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;
/**
* List of MultiServlet.Operation.
*/
private List operations;
/**
* @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("http-method");
if (httpMethod == null) {
throw new ServletException("Missing init-param: http-method");
}
if (httpMethod.equals("GET")) {
this.supportGet = true;
} else 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);
}
// parse operation list
String operationStr = config.getInitParameter("operations");
if (operationStr == null) {
throw new ServletException("Missing init-param: operations");
}
this.operations = parseOperations(operationStr);
}
/**
* Parse an operation line.
*/
private static Operation parseOperationLine(String line)
throws ServletException {
StringTokenizer tokenizer = new StringTokenizer(line, " ");
String[] tokens = new String[3];
int i = 0;
for (; i < tokens.length && tokenizer.hasMoreTokens(); i++) {
String token = tokenizer.nextToken();
tokens[i] = token;
}
if (i < 1) {
throw new ServletException("Not enough tokens on this line: "
+ line);
}
if (tokenizer.hasMoreTokens()) {
throw new ServletException("Too many tokens on this line: " + line);
}
Operation operation = new Operation(tokens[0], tokens[1], tokens[2]);
return operation;
}
/**
* Parse operations config: HTTPMethod nameOrPath [varName]
*/
private static List parseOperations(String operations)
throws ServletException {
List operationList = new ArrayList();
int currentPos = 0;
do {
int nextLineFeed = operations.indexOf('\n', currentPos + 1);
String line;
if (nextLineFeed != -1) {
line = operations.substring(currentPos, nextLineFeed);
} else {
line = operations.substring(currentPos);
}
Operation operation = parseOperationLine(line.trim());
operationList.add(operation);
currentPos = nextLineFeed;
} while (currentPos != -1);
return operationList;
}
/**
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if (this.supportGet) {
processRequest(request, response);
} else {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
/**
* @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);
}
}
/**
* Do the real work.
*/
private void processRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException {
final ServletContext context = getServletContext();
// get a TransactionServlet instance
TransactionServlet transactionServlet = (TransactionServlet) getServletContext()
.getAttribute(TransactionServlet.CONTEXT_ATTRIBUTE_NAME_SERVLET);
if (transactionServlet == null) {
throw new ServletException("Missing TransactionServlet.");
}
// create the parameter resolver that will help us throughout this
// request
final ParameterResolver parameterResolver = new ParameterResolver(
request);
// this flag is true if we create our own transaction, instead of
// using a transaction specified by the client
boolean implicitTransaction = false;
Transaction transaction = null;
try {
// start the transaction
transaction = getTransaction(parameterResolver);
if (transaction == null) {
transaction = transactionServlet.createTransaction();
implicitTransaction = true;
}
// put the transaction in the request scope so AbstractServlet knows
// that requests are part of the transaction.
request.setAttribute(REQUEST_ATTRIBUTE_NAME_TRANSACTION,
transaction);
// loop through operations
for (Iterator i = this.operations.iterator(); i.hasNext();) {
Operation operation = (Operation) i.next();
// get the dispatcher
RequestDispatcher dispatcher = ServletParameterUtils
.getRequestDispatcher(context, operation
.getResourceNameOrPath());
// wrap our request in order to override the HTTP method
RequestWrapper requestWrapper = new RequestWrapper(request,
operation.getHttpMethod());
// if this is the last operation just forward the request
if (!i.hasNext()) {
dispatcher.forward(requestWrapper, response);
break;
}
// wrap our response in order to capture its body
ResponseWrapper responseWrapper = new ResponseWrapper(response);
// otherwise process this operation
dispatcher.include(requestWrapper, responseWrapper);
// check for successful response
int responseStatus = responseWrapper.getStatus();
if (responseStatus < 200 || responseStatus > 299) {
throw new ServletException("Operation "
+ operation.getResourceNameOrPath()
+ " returned HTTP status " + responseStatus);
}
// put the result in request scope
String opName = operation.getName();
if (opName != null && opName.length() > 0) {
String responseBody = responseWrapper.getBody();
request.setAttribute(opName, responseBody);
}
}
// commit the transaction
if (implicitTransaction) {
transaction.commit();
}
} catch (Exception e) {
// rollback the transaction
// if the transaction was started by the client, it is the client
// responsibility to rollback it.
if (transaction != null && implicitTransaction) {
try {
transaction.rollback();
} catch (Exception e2) {
log("Error rolling back transaction.", e2);
}
}
throw new ServletException("Error processing request.", e);
}
}
private static class Operation {
private final String httpMethod;
private final String resourceNameOrPath;
private final String name;
/**
* Constructor.
*
* @param httpMethod
* The HTTP method to use.
* @param resourceNameOrPath
* a resource to dispatch the request to. May be the resource
* name or the resource path.
* @param name
* The optional request attribute name to hold the response
* body.
*/
private Operation(final String httpMethod,
final String resourceNameOrPath, final String name) {
this.httpMethod = httpMethod;
this.resourceNameOrPath = resourceNameOrPath;
this.name = name;
}
/**
* @return Returns the httpMethod.
*/
public String getHttpMethod() {
return this.httpMethod;
}
/**
* @return Returns the name.
*/
public String getName() {
return this.name;
}
/**
* @return Returns the resourceNameOrPath.
*/
public String getResourceNameOrPath() {
return this.resourceNameOrPath;
}
}
/**
* Wraps a HttpServletRequest and override the HTTP method.
*/
private static class RequestWrapper extends HttpServletRequestWrapper {
// TODO add support for in-memory request body
// in order to let multiple servlets use the request body.
private final String httpMethod;
private RequestWrapper(HttpServletRequest request, String httpMethod) {
super(request);
this.httpMethod = httpMethod;
}
/**
* @see javax.servlet.http.HttpServletRequest#getMethod()
*/
public String getMethod() {
// System.out.println("getMethod() " + this.httpMethod);
return this.httpMethod;
}
}
/**
* Wraps a HttpServletResponse in order to capture its response body.
*/
private static class ResponseWrapper extends HttpServletResponseWrapper {
private Writer writer;
private int status = SC_OK;
/**
* Constructor.
*/
private ResponseWrapper(HttpServletResponse response) {
super(response);
}
/**
* @see javax.servlet.ServletResponse#getOutputStream()
*/
public ServletOutputStream getOutputStream() throws IOException {
// TODO implement
throw new UnsupportedOperationException(
"This implementation does not yet support getOutputStream().");
}
/**
* @see javax.servlet.ServletResponse#getWriter()
*/
public PrintWriter getWriter() throws IOException {
if (this.writer != null) {
throw new IOException("PrintWriter has already been obtained.");
}
// we could also use a StringWriter...
// what's the difference?
this.writer = new CharArrayWriter();
return new PrintWriter(this.writer);
}
/**
* @return The body of this response.
*/
private String getBody() {
return this.writer.toString();
}
/**
* @see javax.servlet.http.HttpServletResponseWrapper#setStatus(int)
*/
public void setStatus(int status) {
super.setStatus(status);
this.status = status;
}
/**
* @see javax.servlet.http.HttpServletResponse#setStatus(int,
* java.lang.String)
*/
public void setStatus(int status, String message) {
super.setStatus(status, message);
this.status = status;
}
/**
* @see javax.servlet.http.HttpServletResponse#sendError(int,
* java.lang.String)
*/
public void sendError(int status, String message) throws IOException {
super.sendError(status, message);
this.status = status;
}
/**
* @see javax.servlet.http.HttpServletResponse#sendError(int)
*/
public void sendError(int status) throws IOException {
super.sendError(status);
this.status = status;
}
/**
* @return Returns the status.
*/
protected int getStatus() {
return this.status;
}
}
}