/*
* argun 1.0
* Web 2.0 delivery framework
* Copyright (C) 2007 Hammurapi Group
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* URL: http://www.hammurapi.biz
* e-Mail: support@hammurapi.biz
*/
package biz.hammurapi.web;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import biz.hammurapi.authorization.AuthorizationProvider;
import biz.hammurapi.config.Context;
import biz.hammurapi.metrics.MeasurementCategoryFactory;
import biz.hammurapi.metrics.TimeIntervalCategory;
import biz.hammurapi.web.menu.MenuFilter;
import biz.hammurapi.xml.dom.AbstractDomObject;
import biz.hammurapi.xml.dom.CompositeDomSerializer;
/**
* Dispatching servlet dispatches requests to action methods. Action method is any method which takes 3 parameters:
* HttpServletRequest, HttpServletResponse, DispatchingServlet or two parameters HttpServletRequest and HttpServletResponse.
* Return value of action method is processed in the following way:
* a) If it is instance of Forward then forward is performed
* b) If it is instance of String then it is gets written to response output stream.
* c) Otherwise it is XML-ized and then styled.
*/
public abstract class DispatchingServlet extends StylingServlet {
private static final String AUTH_NONE = "none";
private static final String AUTH_TRUST_MENU = "trust-menu";
private static final String AUTH_STRICT = "strict";
private static final String AUTHORIZATION = "authorization";
private static final String EXCEPTION_HANDLING = "exception-handling";
private static final String PERMISSIONS_ELEMENT = "permissions-element";
private String permissionsElement;
private String exceptionHandling;
private String authorization;
private static final Logger logger=Logger.getLogger(DispatchingServlet.class);
private static final TimeIntervalCategory tic=MeasurementCategoryFactory.getTimeIntervalCategory(DispatchingServlet.class);
private static final TimeIntervalCategory atic=MeasurementCategoryFactory.getTimeIntervalCategory(DispatchingServlet.class.getName()+".action");
private static final String AUTHORIZATION_PROVIDER_ATTRIBUTE = AuthorizationProvider.class.getName();
/**
* Override this method to return custom serializer if needed.
* @return DomSerializer
*/
protected CompositeDomSerializer getDomSerializer() {
return CompositeDomSerializer.getThreadInstance();
}
public void init(ServletConfig config) throws ServletException {
super.init(config);
config.getServletContext().log("Initialization: "+config.getServletName());
authorization = config.getInitParameter(AUTHORIZATION);
if (authorization == null) {
authorization = AUTH_STRICT;
}
if (!(AUTH_STRICT.equals(authorization) || AUTH_NONE.equals(authorization) || AUTH_TRUST_MENU.equals(authorization))) {
throw new ServletException("Invalid authorization mode: "+authorization);
}
String initParameter = config.getInitParameter(PERMISSIONS_ELEMENT);
if (initParameter!=null) {
permissionsElement=initParameter;
config.getServletContext().log("permissions-element="+permissionsElement);
}
initParameter = config.getInitParameter(EXCEPTION_HANDLING);
if (initParameter!=null) {
exceptionHandling=initParameter;
config.getServletContext().log("exception-handling="+exceptionHandling);
}
config.getServletContext().setAttribute("servlet/"+config.getServletName(), this);
}
/**
* Extracts instance name from the path and returns instance for the path.
* @param actionPath
* @return Object to invoke action methods
* @throws HammurapiWebException
*/
protected abstract Object getActionInstance(String path) throws HammurapiWebException;
/**
* @param path
* @return Action method name
* @throws HammurapiWebException
*/
protected abstract String getActionName(String path) throws HammurapiWebException;
/**
* @return Position of "/" in the servlet path info which separates action name from style info.
*/
protected abstract int styleSeparatorPosition();
private Map actions=new HashMap();
private class Action {
private Method method;
private Object instance;
private int argCount;
private boolean isContextMethod; // true if the first parameter is context
public Action(Method method, Object instance) {
super();
this.method = method;
this.instance = instance;
argCount = method.getParameterTypes().length;
isContextMethod = Context.class.equals(method.getParameterTypes()[0]);
}
public Object execute(HttpServletRequest request, HttpServletResponse response, String path) throws HammurapiWebException {
// Do authorization
if (AUTH_STRICT.equals(authorization) || (AUTH_TRUST_MENU.equals(authorization) && request.getAttribute(MenuFilter.MENU_MATCHED_ATTRIBUTE)==null)) {
Object ap = request.getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
if (ap == null) {
HttpSession session = request.getSession(false);
ap = session == null ? null : session.getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
}
if (ap instanceof AuthorizationProvider) {
if (!((AuthorizationProvider) ap).hasInstancePermission(instance, method.getName())) {
return "You are not authorized to access this page"; // Replace with proper Http error if needed.
}
} else if (ap!=null) {
logger.warn("Configuration problem with authorization provider: "+ ap.getClass().getName()+" does not implement "+AuthorizationProvider.class.getName());
}
}
long start=atic.getTime();
try {
Object[] args;
if (isContextMethod) {
RequestContext rc = new RequestContext(request);
switch (argCount) {
case 1:
args=new Object[] {rc};
break;
case 2:
args=new Object[] {rc, DispatchingServlet.this};
break;
case 3:
args=new Object[] {rc, DispatchingServlet.this, path};
break;
default:
throw new HammurapiWebException("Invalid number of arguments in method "+method);
}
} else {
switch (argCount) {
case 2:
args=new Object[] {request, response};
break;
case 3:
args=new Object[] {request, response, DispatchingServlet.this};
break;
case 4:
args=new Object[] {request, response, DispatchingServlet.this, path};
break;
default:
throw new HammurapiWebException("Invalid number of arguments in method "+method);
}
}
return method.invoke(instance, args);
} catch (IllegalAccessException e) {
logger.error("Exception is action "+method.toString(), e);
throw new HammurapiWebException(e);
} catch (InvocationTargetException e) {
logger.error("Exception is action "+method.toString(), e);
if (e.getCause()!=null) {
if ("message".equals(exceptionHandling)) {
return e.getCause().getMessage();
} else if ("to-string".equals(exceptionHandling)) {
return e.getCause().toString();
} else if ("stack-trace".equals(exceptionHandling)) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.getCause().printStackTrace(pw);
pw.close();
try {
sw.close();
} catch (IOException e1) {
throw new HammurapiWebException("Should never ever happen", e1);
}
return "<pre>"+sw.toString()+"</pre>";
} else if ("cause-message".equals(exceptionHandling)) {
return getToTheCause(e).getMessage();
} else if ("cause-to-string".equals(exceptionHandling)) {
return getToTheCause(e).toString();
} else if ("cause-stack-trace".equals(exceptionHandling)) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
getToTheCause(e).printStackTrace(pw);
pw.close();
try {
sw.close();
} catch (IOException e1) {
throw new HammurapiWebException("Should never ever happen", e1);
}
return "<pre>"+sw.toString()+"</pre>";
}
}
throw new HammurapiWebException(e);
} finally {
atic.addInterval(method.toString(), start);
}
}
/**
* Executes action without authorization checks
* @param ctx - Context, typically request context
* @param path - action path tail
* @return
* @throws HammurapiWebException
*/
public Object execute(Context ctx, String path) throws HammurapiWebException {
long start=atic.getTime();
try {
Object[] args;
if (isContextMethod) {
switch (argCount) {
case 1:
args=new Object[] {ctx};
break;
case 2:
args=new Object[] {ctx, DispatchingServlet.this};
break;
case 3:
args=new Object[] {ctx, DispatchingServlet.this, path};
break;
default:
throw new HammurapiWebException("Invalid number of arguments in method "+method);
}
} else {
throw new HammurapiWebException("Cannot execute non-context method: "+method);
}
return method.invoke(instance, args);
} catch (IllegalAccessException e) {
logger.error("Exception is action "+method.toString(), e);
throw new HammurapiWebException(e);
} catch (InvocationTargetException e) {
logger.error("Exception is action "+method.toString(), e);
if (e.getCause()!=null) {
if ("message".equals(exceptionHandling)) {
return e.getCause().getMessage();
} else if ("to-string".equals(exceptionHandling)) {
return e.getCause().toString();
} else if ("stack-trace".equals(exceptionHandling)) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.getCause().printStackTrace(pw);
pw.close();
try {
sw.close();
} catch (IOException e1) {
throw new HammurapiWebException("Should never ever happen", e1);
}
return "<pre>"+sw.toString()+"</pre>";
} else if ("cause-message".equals(exceptionHandling)) {
return getToTheCause(e).getMessage();
} else if ("cause-to-string".equals(exceptionHandling)) {
return getToTheCause(e).toString();
} else if ("cause-stack-trace".equals(exceptionHandling)) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
getToTheCause(e).printStackTrace(pw);
pw.close();
try {
sw.close();
} catch (IOException e1) {
throw new HammurapiWebException("Should never ever happen", e1);
}
return "<pre>"+sw.toString()+"</pre>";
}
}
throw new HammurapiWebException(e);
} finally {
atic.addInterval(method.toString(), start);
}
}
}
private Throwable getToTheCause(Throwable th) {
while (th.getCause()!=null && th.getCause()!=th) {
th = th.getCause();
}
return th;
}
/**
* Subclasses can override this method and throw an exception of methods
* which are not supposed to be dispatched to.
* @param method
* @throws HammurapiWebException
*/
protected void verifyMethod(Method method) throws HammurapiWebException {
}
private synchronized Action getAction(String path) throws HammurapiWebException {
Action ret=(Action) actions.get(path);
if (ret==null) {
Object instance = getActionInstance(path);
if (instance == null) {
throw new HammurapiWebException("Action instance not found for path "+path);
}
String actionName = getActionName(path);
Method candidate = null;
Method[] methods = instance.getClass().getMethods();
for (int i=0; i<methods.length; ++i) {
Method method = methods[i];
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length==4
&& method.getName().equals(actionName)
&& HttpServletRequest.class.equals(parameterTypes[0])
&& HttpServletResponse.class.equals(parameterTypes[1])
&& parameterTypes[2].isInstance(this)
&& String.class.equals(parameterTypes[3])) {
candidate = method;
break;
}
}
if (candidate == null) {
for (int i=0; i<methods.length; ++i) {
Method method = methods[i];
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length==3
&& method.getName().equals(actionName)
&& HttpServletRequest.class.equals(parameterTypes[0])
&& HttpServletResponse.class.equals(parameterTypes[1])
&& parameterTypes[2].isInstance(this)) {
candidate = method;
break;
}
}
}
if (candidate == null) {
for (int i=0; i<methods.length; ++i) {
Method method = methods[i];
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length==2
&& method.getName().equals(actionName)
&& HttpServletRequest.class.equals(parameterTypes[0])
&& HttpServletResponse.class.equals(parameterTypes[1])) {
candidate = method;
break;
}
}
}
// Methods which take context instead or request and response
for (int i=0; i<methods.length; ++i) {
Method method = methods[i];
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length==3
&& method.getName().equals(actionName)
&& Context.class.equals(parameterTypes[0])
&& parameterTypes[2].isInstance(this)
&& String.class.equals(parameterTypes[3])) {
candidate = method;
break;
}
}
if (candidate == null) {
for (int i=0; i<methods.length; ++i) {
Method method = methods[i];
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length==2
&& method.getName().equals(actionName)
&& Context.class.equals(parameterTypes[0])
&& parameterTypes[2].isInstance(this)) {
candidate = method;
break;
}
}
}
if (candidate == null) {
for (int i=0; i<methods.length; ++i) {
Method method = methods[i];
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length==1
&& method.getName().equals(actionName)
&& Context.class.equals(parameterTypes[0])) {
candidate = method;
break;
}
}
}
if (candidate == null) {
throw new HammurapiWebException("Action method "+actionName+" not found for path "+path+" in class "+instance.getClass().getName());
}
ret=new Action(candidate, instance);
actions.put(path, ret);
}
return ret;
}
/** Handles the HTTP <code>GET</code> method.
* @param request servlet request
* @param response servlet response
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
processRequest(request, response);
}
/** Handles the HTTP <code>POST</code> method.
* @param request servlet request
* @param response servlet response
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
processRequest(request, response);
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long start=tic.getTime();
String pathInfo=request.getPathInfo();
logger.debug("Serving "+pathInfo);
try {
if (pathInfo==null || "/".equals(pathInfo)) {
response.sendError(404, "Invalid action path");
return;
}
int styleIdx=pathInfo.indexOf("/", styleSeparatorPosition());
if (styleIdx==-1) {
response.sendError(404, "Invalid action path");
return;
}
styleIdx=pathInfo.indexOf("/", styleIdx+1);
Action action=getAction(styleIdx==-1 ? pathInfo.substring(1) : pathInfo.substring(1, styleIdx));
String styleName=styleIdx==-1 ? null : pathInfo.substring(styleIdx+1);
Object ret=action.execute(request, response, styleName);
if (ret==null) {
return;
}
if (ret instanceof Forward) {
getServletContext().getRequestDispatcher(((Forward) ret).getUri()).forward(request, response);
return;
}
if (ret instanceof Include) {
getServletContext().getRequestDispatcher(((Include) ret).getUri()).include(request, response);
return;
}
if (ret instanceof Redirect) {
Redirect redirect = (Redirect) ret;
response.sendRedirect(redirect.getLocation());
PrintWriter out = response.getWriter();
out.write("<HTML><HEAD></HEAD><BODY>");
out.write(redirect.getMessage()==null ? "Redirecting" : redirect.getMessage());
out.write("</BODY></HTML>");
return;
}
if (ret instanceof HttpError) {
HttpError error = (HttpError) ret;
if (error.getMessage()==null) {
response.sendError(error.getErrorCode());
} else {
response.sendError(error.getErrorCode(), error.getMessage());
}
return;
}
if (ret instanceof String) {
Writer w=new OutputStreamWriter(response.getOutputStream());
w.write((String) ret);
w.close();
return;
}
style(ret, styleName, request, response);
} catch (HammurapiWebException e) {
logger.error("Exception serving '"+pathInfo+"': "+e, e);
throw new ServletException(e);
} catch (ParserConfigurationException e) {
throw new ServletException(e);
} finally {
tic.addInterval(pathInfo, start);
}
}
public String executeAction(String pathInfo, Context ctx) throws ServletException, IOException {
long start=tic.getTime();
logger.debug("Serving "+pathInfo);
try {
if (pathInfo==null || "/".equals(pathInfo)) {
return "Invalid action path";
}
int styleIdx=pathInfo.indexOf("/", styleSeparatorPosition());
if (styleIdx==-1) {
return "Invalid action path";
}
styleIdx=pathInfo.indexOf("/", styleIdx+1);
Action action=getAction(styleIdx==-1 ? pathInfo : pathInfo.substring(0, styleIdx));
String styleName=styleIdx==-1 ? null : pathInfo.substring(styleIdx+1);
Object ret=action.execute(ctx, styleName);
if (ret==null) {
return null;
}
if (ret instanceof Forward) {
return "Forward: "+((Forward) ret).getUri();
}
if (ret instanceof Include) {
return "Include: "+((Include) ret).getUri();
}
if (ret instanceof Redirect) {
Redirect redirect = (Redirect) ret;
return "Redirect to "+redirect.getLocation()+" with message "+redirect.getMessage();
}
if (ret instanceof HttpError) {
HttpError error = (HttpError) ret;
if (error.getMessage()==null) {
return "Error "+error.getErrorCode();
}
return "Error "+error.getErrorCode()+": "+ error.getMessage();
}
if (ret instanceof String) {
return (String) ret;
}
StringWriter sw = new StringWriter();
style(ret, styleName, ctx, new StreamResult(sw));
sw.close();
return sw.toString();
} catch (HammurapiWebException e) {
logger.error("Exception serving '"+pathInfo+"': "+e, e);
throw new ServletException(e);
} catch (ParserConfigurationException e) {
throw new ServletException(e);
} finally {
tic.addInterval(pathInfo, start);
}
}
/**
* Converts object returned from action to XML and applies style
* @param ret Object to be styled
* @param styleName Style name
* @param request request
* @param response response
* @throws ParserConfigurationException
* @throws FactoryConfigurationError
* @throws ServletException
*/
public void style(Object ret, String styleName, HttpServletRequest request, HttpServletResponse response) throws ParserConfigurationException, FactoryConfigurationError, ServletException {
if (ret instanceof Document) {
getTransformer(styleName).transform((Node) ret, getSetParametersCallback(request, styleName), response);
} else {
Document doc=newDocumentBuilder().newDocument();
Element re = doc.createElement("response");
re.setAttribute("context-path", request.getContextPath());
doc.appendChild(re);
getDomSerializer().toDomSerializable(ret).toDom(re);
if (permissionsElement!=null) {
Object ap = request.getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
if (ap == null) {
HttpSession session = request.getSession(false);
ap = session == null ? null : session.getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
}
if (ap instanceof AuthorizationProvider) {
getDomSerializer()
.toDomSerializable(((AuthorizationProvider) ap).getPermissions())
.toDom(AbstractDomObject.addElement(re, permissionsElement));
} else if (ap!=null) {
logger.warn("Configuration problem with authorization provider: "+ ap.getClass().getName()+" does not implement "+AuthorizationProvider.class.getName());
}
}
getTransformer(styleName).transform(doc, getSetParametersCallback(request, styleName), response);
}
}
/**
* Converts object returned from action to XML and applies style
* @param ret Object to be styled
* @param styleName Style name
* @param request request
* @param response response
* @throws ParserConfigurationException
* @throws FactoryConfigurationError
* @throws ServletException
*/
public void style(Object ret, String styleName, Context context, Result result) throws ParserConfigurationException, HammurapiWebException {
Document doc;
if (ret instanceof Document) {
doc=(Document) ret;
} else {
doc=newDocumentBuilder().newDocument();
Element re = doc.createElement("response");
String contextPath = (String) context.get("context-path");
if (contextPath!=null) {
re.setAttribute("context-path", contextPath);
}
doc.appendChild(re);
getDomSerializer().toDomSerializable(ret).toDom(re);
}
getTransformer(styleName).transform(doc, result);
}
}
|