/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.web.controller;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.scooterframework.admin.ApplicationConfig;
import com.scooterframework.admin.Constants;
import com.scooterframework.admin.EnvConfig;
import com.scooterframework.admin.FilterManager;
import com.scooterframework.admin.FilterManagerFactory;
import com.scooterframework.autoloader.JavaCompiler;
import com.scooterframework.common.exception.ExecutionException;
import com.scooterframework.common.exception.MethodCreationException;
import com.scooterframework.common.logging.LogUtil;
import com.scooterframework.common.util.CurrentThreadCache;
import com.scooterframework.common.util.CurrentThreadCacheClient;
import com.scooterframework.common.util.StringUtil;
import com.scooterframework.common.util.WordUtil;
import com.scooterframework.orm.sqldataexpress.config.DatabaseConfig;
import com.scooterframework.web.route.NoRouteFoundException;
/**
* <p><strong>BaseRequestProcessor</strong> contains the processing logic that
* the {@link MainActionServlet} performs as it receives each servlet request
* from the container. You can customize the request processing behavior by
* subclassing this class and overriding the method(s) whose behavior you are
* interested in changing.</p>
*
* @author (Fei) John Chen
*/
public class BaseRequestProcessor {
protected LogUtil log = LogUtil.getLogger(getClass().getName());
private Map<String, ActionProperties> requestPropertiesMap = new HashMap<String, ActionProperties>();
public static final String DEFAULT_CONTROLLER_CLASS = "com.scooterframework.builtin.CRUDController";
public static final String EXECUTION_INTERRUPTED = "EXECUTION_INTERRUPTED";
/**
* Constructor
*/
public BaseRequestProcessor() {
}
/**
* <p>Process an <tt>HttpServletRequest</tt> and create the
* corresponding <tt>HttpServletResponse</tt> or dispatch
* to another resource.</p>
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
public void process(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
if (log.isDebugEnabled()) displayHttpRequest(request);
try {
processLocale(request, response);
String requestPath = CurrentThreadCacheClient.requestPath();
if (!isAdminRequest(requestPath) && JavaCompiler.hasCompileErrors()) {
processCompileError(request, response);
return;
}
if (isRootAccess(requestPath)) {
processRootAccess(request, response);
}
else {
String result = null;
String requstPathKey = CurrentThreadCacheClient.requestPathKey();
ActionProperties aps = requestPropertiesMap.get(requstPathKey);
if (aps == null || ApplicationConfig.getInstance().isInDevelopmentEnvironment()) {
String requestHttpMethod = CurrentThreadCacheClient.httpMethod();
aps = prepareActionProperties(requestPath, requestHttpMethod, request);
registerActionProperties(request, aps);
requestPropertiesMap.put(requstPathKey, aps);
}
else {
registerActionProperties(request, aps);
}
log.debug("aps: " + aps);
result = executeRequest(aps, request, response);
log.debug("execution result: " + result);
if (result != null) {
processNotNullResult(request, response, aps, result);
}
else {
processNullResult(request, response, aps);
}
}
}
catch(Exception ex) {
processException(request, response, ex);
}
}
private boolean isAdminRequest(String requestPath) {
return (requestPath != null && requestPath.toLowerCase().startsWith("/admin"));
}
/**
* <p>Process an <tt>HttpServletRequest</tt>.</p>
*
* @param aps properties of request
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @return execution result
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
public String executeRequest(ActionProperties aps,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
Object controllerInstance = null;
if (aps.controllerCreated) {
controllerInstance = aps.controllerInstance;
}
else {
controllerInstance = getControllerInstance(aps.controllerClassName);
aps.controllerInstance = controllerInstance;
aps.controllerCreated = true;
}
if (controllerInstance == null) {
if (EnvConfig.getInstance().allowForwardToControllerNameViewWhenControllerNotExist()) {
log.info("Controller instance for \"" + aps.controller +
"\" does not exist, forward to view \"" + aps.controller +
File.separator + aps.action + "\".");
return null;
}
else {
throw new NoControllerFoundException(aps.controllerClassName);
}
}
Method actionInstance = null;
if (aps.methodCreated) {
actionInstance = aps.methodInstance;
}
else {
actionInstance = getActionMethod(controllerInstance.getClass(), aps.action);
aps.methodInstance = actionInstance;
aps.methodCreated = true;
}
if (actionInstance == null) {
if (EnvConfig.getInstance().allowForwardToActionNameViewWhenActionNotExist()) {
log.debug("Action method \"" + aps.action +
"\", forward to view \"" + aps.action + "\".");
return null;
}
else {
throw new MethodCreationException(controllerInstance.getClass().getName(), aps.action);
}
}
return executeControllerAction(controllerInstance, actionInstance);
}
/**
* Sets up action properties for the action execution. The properties are
* wrapped up in an <tt>ActionProperties</tt> instance.
*
* @param request The servlet request we are processing
* @return an ActionProperties instance
*/
public ActionProperties prepareActionProperties(String requestPath,
String requestHttpMethod, HttpServletRequest request) {
String path = requestPath;
String controllerPath = null;
String controller = null;
String action = null;
String format = null;
int lastDot = path.lastIndexOf(".");
int lastSlash = path.lastIndexOf("/");
if (lastDot != -1 && lastDot > lastSlash) {
format = path.substring(lastDot + 1);
if (EnvConfig.getInstance().hasMimeTypeFor(format)) {
path = path.substring(0, lastDot);
}
else {
format = null;
}
}
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
lastSlash = path.lastIndexOf("/");
if (lastSlash > 0) {
action = path.substring(lastSlash + 1);
controllerPath = path.substring(0, lastSlash);
lastSlash = controllerPath.lastIndexOf("/");
if (lastSlash != -1) {
controller = controllerPath.substring(lastSlash + 1);
}
}
else if (lastSlash == 0) {
controllerPath = path;
controller = path.substring(1);
}
if (action == null || "".equals(action)) {
if (EnvConfig.getInstance().allowDefaultActionMethod()) {
action = EnvConfig.getInstance().getDefaultActionMethod();
}
else {
throw new IllegalArgumentException("The value for action " +
"is not detected from the request path \"" + requestPath +
"\" and the default action method is not allowed in property file.");
}
}
ActionProperties aps = new ActionProperties();
aps.controllerPath = controllerPath;
aps.controller = controller;
aps.controllerClassName = getControllerClassName(controllerPath);
aps.action = action;
aps.model = (DatabaseConfig.getInstance().usePluralTableName())?WordUtil.singularize(controller):controller;
aps.format = format;
return aps;
}
/**
* Puts some action properties in <tt>request</tt> object.
*/
protected void registerActionProperties(HttpServletRequest request, ActionProperties aps) {
CurrentThreadCacheClient.cacheController(aps.controller);
CurrentThreadCacheClient.cacheControllerClass(aps.controllerClassName);
CurrentThreadCacheClient.cacheControllerPath(aps.controllerPath);
CurrentThreadCacheClient.cacheAction(aps.action);
CurrentThreadCacheClient.cacheModel(aps.model);
CurrentThreadCacheClient.cacheFormat(aps.format);
request.setAttribute(Constants.CONTROLLER, aps.controller);
request.setAttribute(Constants.CONTROLLER_CLASS, aps.controllerClassName);
request.setAttribute(Constants.CONTROLLER_PATH, aps.controllerPath);
request.setAttribute(Constants.ACTION, aps.action);
request.setAttribute(Constants.MODEL, aps.model);
request.setAttribute(Constants.FORMAT, aps.format);
}
/**
* Checks if a request is local. Subclass can override this method if a
* different logic is used to determine local request.
*
* @param request HttpServletRequest
* @return true if the request is from a localhost
*/
protected boolean isLocalRequest(HttpServletRequest request) {
String s = (String)CurrentThreadCache.get(Constants.LOCAL_REQUEST);
if ((Constants.VALUE_FOR_LOCAL_REQUEST).equals(s)) {
return true;
}
return false;
}
protected void processLocale(HttpServletRequest request,
HttpServletResponse response) {
//use the requested locale
Locale locale = request.getLocale();
if (locale == null) {
locale = ACH.getAC().getLocale(ActionContext.SCOPE_SESSION);
if (locale != null) {
//there is no need to do anything if a locale has been chosen.
return;
}
else {
//use the configured locale
locale = ActionContext.getGlobalLocale();
if (locale == null) {
locale = Locale.getDefault();
}
ACH.getAC().setLocale(locale, ActionContext.SCOPE_SESSION);
}
}
else {
Locale sessionLocale = ACH.getAC().getLocale(ActionContext.SCOPE_SESSION);
if (sessionLocale == null || !sessionLocale.equals(locale)) {
ACH.getAC().setLocale(locale, ActionContext.SCOPE_SESSION);
}
}
log.debug("User locale is '" + locale + "'.");
}
/**
* Returns a controller class name.
*
* @param controllerPath controller path
* @return controller class name
*/
protected String getControllerClassName(String controllerPath) {
String controllerClassName = EnvConfig.getInstance().getControllerClassName(controllerPath);
return controllerClassName;
}
/**
* Returns a controller instance.
*
* @param controllerClassName controller class name
* @return controller instance
*/
protected Object getControllerInstance(String controllerClassName) {
return ControllerFactory.createController(controllerClassName, getDefaultControllerClassName());
}
/**
* Returns class name of default controller. This default controller is used
* when application specific controller is not available and the
* <tt>auto.crud</tt> property is set to true in the environment.properties
* file. Subclass must override this method if a different default
* controller class is used.
*/
protected String getDefaultControllerClassName() {
return DEFAULT_CONTROLLER_CLASS;
}
/**
* Returns a method instance related to an action of a controller.
*
* @param controllerClass a controller class type
* @param actionName name of the action method
* @return the method instance
*/
protected Method getActionMethod(Class<?> controllerClass, String actionName) {
if (controllerClass == null || actionName == null) return null;
Method method = null;
try {
method = ControllerFactory.getMethod(controllerClass, actionName);
}
catch(Exception ex) {
log.debug("Failed to create action method instance: " + ex.getMessage());
}
return method;
}
/**
* Invokes an action method of a controller.
*
* @param controller The controller instance to be invoked
* @param method The action method
* @return execution result
*/
protected String executeControllerAction(Object controller, Method method) {
if (controller == null || method == null) return null;
String result = null;
try {
boolean beforeIsSuccess = true;
FilterManager filterManager = FilterManagerFactory.getInstance().getFilterManager(controller.getClass());
if (filterManager != null && !filterManager.noFilterDeclared()) {
result = filterManager.executeBeforeFiltersOn(method.getName());
if (result != null) beforeIsSuccess = false;
}
if (beforeIsSuccess) {
result = (String)method.invoke(controller, (Object[])null);
}
if (beforeIsSuccess && filterManager != null && !filterManager.noFilterDeclared()) {
String afResult = filterManager.executeAfterFiltersOn(method.getName());
if (afResult != null) {
result = afResult;
}
}
} catch (Exception ex) {
log.error("Error in executeControllerAction controller/action: " + controller + "/" + method, ex);
ExecutionException eex =
new ExecutionException(controller.getClass().getName(), method.getName(), null, ex);
throw eex;
}
return result;
}
/**
* <p>Processes not-null result of an action method. "not-null" result is a
* result string tagged by one of the supporting tags. All supported tags
* are documented in ActionResult.</p>
*
* <pre>
* Examples of not-null results:
*
* Forward result to a view:
* forwardTo=>/WEB-INF/views/jsp/sayit.jsp
*
* Display result in html format:
* html=><h1>Good morning</h1>
*
* Return xml formatted document:
* xml=><?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><book><title>Java Programming</title><price>$50</price></book>
*
* Return plain-text document:
* text=>This is a small world.
* </pre>
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param aps properties of request
* @param result tagged result of an action
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processNotNullResult(HttpServletRequest request,
HttpServletResponse response,
ActionProperties aps,
String result)
throws IOException, ServletException
{
if (hasRendered(request)) {
return;
}
if (ActionResult.checkResultTag(result, ActionResult.TAG_REDIRECT_TO)) {
processResultRedirect(request, response, result);
}
else if (ActionResult.checkResultTag(result, ActionResult.TAG_FORWARD_TO)) {
processResultForward(request, response, result);
}
else if (ActionResult.checkResultTag(result, ActionResult.TAG_ERROR)) {
processResultError(request, response, result);
}
else {
if (ActionResult.startsWithContentTypeTag(result)) {
String tag = ActionResult.getContentTypeTag(result);
String content = ActionResult.getResultContentByTag(result, tag);
processResultContentForRequestFormatType(request, response, content, tag);
}
else {
String tag = (aps.format != null)?aps.format:Constants.DEFAULT_RESPONSE_FORMAT;
processResultContentForRequestFormatType(request, response, result, tag);
}
}
}
protected void processResultContentForRequestFormatType(
HttpServletRequest request,
HttpServletResponse response,
String content,
String format)
throws IOException, ServletException
{
ContentHandler handler = ContentHandlerFactory.getContentHandler(format);
if (handler != null) {
handler.handle(request, response, content, format);
}
else {
throw new IllegalArgumentException(
"There is no handler found for format \""
+ format
+ "\". You may create your own as a plugin by "
+ "extending the Plugin class and "
+ "implementing the ContentHandler interface.");
}
}
/**
* Processes error result.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param result
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processResultError(HttpServletRequest request, HttpServletResponse response, String result)
throws IOException, ServletException {
String message = ActionResult.getResultContentByTag(result, ActionResult.TAG_ERROR);
processError(request, response, message);
}
/**
* Processes redirect result.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param result result of action
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processResultRedirect(HttpServletRequest request, HttpServletResponse response, String result)
throws IOException, ServletException {
String target = ActionResult.getResultContentByTag(result, ActionResult.TAG_REDIRECT_TO);
if (target.startsWith("/")) {
String contextPath = request.getContextPath();
if (!target.startsWith(contextPath)) target = contextPath + target;
}
response.sendRedirect(response.encodeRedirectURL(target));
}
/**
* Processes forward result.
*
* Forwards to a URI denoted by the <tt>result</tt>.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param result result of action
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processResultForward(HttpServletRequest request, HttpServletResponse response, String result)
throws IOException, ServletException {
String target = ActionResult.getResultContentByTag(result, ActionResult.TAG_FORWARD_TO);
doForward(target, request, response);
}
/**
* Processes null result.
*
* Forwards to a default URI derived based on controller and action names.
* See <tt>getDefaultViewUri</tt> method.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param aps properties of request
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processNullResult(HttpServletRequest request,
HttpServletResponse response,
ActionProperties aps)
throws IOException, ServletException {
if (hasRendered(request)) return;
String target = getViewURI(StringUtil.toLowerCase(aps.controller), StringUtil.toLowerCase(aps.action));
doForward(target, request, response);
}
private boolean hasRendered(HttpServletRequest request) {
return (request.getAttribute(Constants.REQUEST_RENDERED) != null)?true:false;
}
/**
* Processes root access.
*
* Forwards to a default URI derived based on controller and action names.
* See <tt>getDefaultViewUri</tt> method.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processRootAccess(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String target = EnvConfig.getInstance().getRootURL();
doForward(target, request, response);
}
/**
* Processes an error message.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param error an error message
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processError(HttpServletRequest request, HttpServletResponse response, String error)
throws IOException, ServletException {
String message = "Error in \"" + CurrentThreadCache.get(Constants.REQUEST_PATH) + "\": " + error;
log.error(message);
CurrentThreadCache.set(Constants.ERROR_MESSAGE, message);
doForwardToErrorPage(request, response);
}
/**
* Processes a compile error message.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processCompileError(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
doForwardToCompileErrorPage(request, response);
}
/**
* Processes an exception.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param ex
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void processException(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws IOException, ServletException {
String message = "";
if (ex instanceof NoRouteFoundException) {
message = ex.getMessage();
}
else {
message = "Error in \"" + CurrentThreadCache.get(Constants.REQUEST_PATH) + "\": " + ex.toString();
}
log.error(message);
if (!interpretException(ex)) {
ex.printStackTrace();
}
CurrentThreadCache.set(Constants.ERROR_EXCEPTION, ex);
CurrentThreadCache.set(Constants.ERROR_MESSAGE, message);
//response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
doForwardToErrorPage(request, response);
}
protected boolean interpretException(Exception ex) {
boolean understand = false;
String error = ex.getMessage();
if (error == null || "".equals(error)) return false;
String meaning = "";
if (error.indexOf("Connections could not be acquired from the underlying database!") != -1) {
meaning = "Please verify database connection setup or existence of jdbc library for the database.";
log.error(meaning);
ActionControl.flash("error", meaning);
understand = true;
}
else if (ex instanceof NoRouteFoundException) {
meaning = "Please verify either the route is missing or a view path is setup correctly.";
log.error(meaning);
ActionControl.flash("error", meaning);
understand = true;
}
else if (ex instanceof NoTemplateHandlerException) {
meaning = "Please add a template handler for template type \"" +
((NoTemplateHandlerException)ex).getTemplateType() + "\".";
log.error(meaning);
ActionControl.flash("error", meaning);
understand = true;
}
else if (ex instanceof NoViewFileException) {
meaning = "Please add a view file for view \"" +
((NoViewFileException)ex).getTargetView() + "\".";
log.error(meaning);
ActionControl.flash("error", meaning);
understand = true;
}
return understand;
}
/**
* Returns a default view URI. This URI is actually a real view file
* associated with the controller and the action method name.
*
* @param controller the name of the controller
* @param action the action method
* @return view URI.
*/
protected String getViewURI(String controller, String action) {
String uri = EnvConfig.getViewURI(controller, action, getDefaultViewFilesDirectoryName());
return uri;
}
/**
* Returns default view file directory name.
*
* @return default view file directory name.
*/
protected String getDefaultViewFilesDirectoryName() {
return (EnvConfig.getInstance().allowAutoCRUD())?
EnvConfig.getInstance().getDefaultViewFilesDirectory():null;
}
protected void doForwardToErrorPage(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
doForward(EnvConfig.getInstance().getErrorPageURI(), request, response);
}
protected void doForwardToCompileErrorPage(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
doForward(EnvConfig.getInstance().getCompileErrorPageURI(), request, response);
}
/**
* <p>Do a forward to specified URI using a <tt>RequestDispatcher</tt>.
* This method is used by all internal method needing to do a forward.</p>
*
* @param uri Context-relative URI to forward to
* @param request HTTP servlet request
* @param response HTTP servlet response
* @throws java.io.IOException
* @throws javax.servlet.ServletException
*/
protected void doForward(
String uri,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
ActionControl.doForward(uri, request, response);
}
/**
* Displays all parameter names and theirs values in a HTTP request.
*
* @param request
*/
public void displayHttpRequest(HttpServletRequest request) {
if (request != null) {
@SuppressWarnings("unchecked")
Enumeration<String> en = request.getParameterNames();
while(en.hasMoreElements()) {
String key = en.nextElement();
String[] values = request.getParameterValues(key);
if (values != null) {
String tmp = "";
int length = values.length;
for (int i = 0; i < length - 1; i++) {
tmp += values[i] + ", ";
}
tmp += values[length-1];
if ("password".equalsIgnoreCase(key)) tmp = "********";
log.debug("name=["+key+"] values=["+tmp+"]");
}
}
}
}
/**
* Returns ServletContext
*/
protected ServletContext getServletContext() {
return ACH.getWAC().getHttpServletRequest().getSession().getServletContext();
}
/**
* Returns RealPath
*/
protected String getRealPath() {
return getServletContext().getRealPath("");
}
/**
* Checks if a request path is a root access. A root access means the
* <tt>requestPath</tt> is either "/" or starts with "<tt>/index</tt>".
* @param requestPath
* @return true if the request path is root path.
*/
protected boolean isRootAccess(String requestPath) {
boolean rootAccess = false;
if ("".equals(requestPath) ||
"/".equals(requestPath) || requestPath.startsWith("/index")) {
rootAccess = true;
}
return rootAccess;
}
}