Home

This Chapter
-Chapter 22: How Struts Works
-The Action Servlet
-Creating A ModuleConfig Object
-The Request Processor
-Summary

Table of Contents
-Introduction
-Chapter 1: Model 2 and Struts
-Chapter 2: Input Validation with Action Forms
-Chapter 3: The HTML Tag Library
-Chapter 4: Input Validation and Data Conversion
-Chapter 5: The Validator Plugin
-Chapter 6: The Expression Language
-Chapter 7: JSTL
-Chapter 8: The Bean Tag Library
-Chapter 9: The Logic Tag Library
-Chapter 10: Struts-EL, Nested, selectLabel
-Chapter 11: Message Handling and Internationalization
-Chapter 12: The Tiles Framework
-Chapter 13: Securing Struts Applications
-Chapter 14: The Config Object
-Chapter 15: The Persistence Layer
-Chapter 16: Object Caching
-Chapter 17: File Upload and File Download
-Chapter 18: Paging and Sorting
-Chapter 19: Preventing Double Submits
-Chapter 20: Early HttpSession Invalidation
-Chapter 21: Decorating Request Objects
-Chapter 22: How Struts Works

Previous
Next

 

The Request Processor

The request processor, through its process method, processes every Struts action invocation. The process method is given in Listing 22.2.

Listing 22.2: The request processor’s process method

public void process(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
  // Wrap multipart requests with a special wrapper
  request = processMultipart(request);

  // Identify the path component we will use to select a mapping
  String path = processPath(request, response);
  if (path == null) {
    return;
  }

  if (log.isDebugEnabled()) {
    log.debug("Processing a '" + request.getMethod() +
    "' for path '" + path + "'");
  }

  // Select a Locale for the current user if requested
  processLocale(request, response);

  // Set the content type and no-caching headers if requested
  processContent(request, response);
  processNoCache(request, response);

  // General purpose preprocessing hook
  if (!processPreprocess(request, response)) {
    return;
  }

  this.processCachedMessages(request, response);

  // Identify the mapping for this request
  ActionMapping mapping = processMapping(request, response, path);
  if (mapping == null) {
    return;
  }

  // Check for any role required to perform this action
  if (!processRoles(request, response, mapping)) {
    return;
  }

  // Process any ActionForm bean related to this request
  ActionForm form = processActionForm(request, response, mapping);
  processPopulate(request, response, form, mapping);
  if (!processValidate(request, response, form, mapping)) {
    return;
  }

  // Process a forward or include specified by this mapping
  if (!processForward(request, response, mapping)) {
    return;
  }

  if (!processInclude(request, response, mapping)) {
    return;
  }

  // Create or acquire the Action instance to process this request
  Action action = processActionCreate(request, response, mapping);
  if (action == null) {
    return;
  }

  // Call the Action instance itself
  ActionForward forward =
  processActionPerform(request, response, action, form, mapping);

  // Process the returned ActionForward instance
  processForwardConfig(request, response, forward);
}

The subsections that follow discuss the methods called by the process method. Along the way, you will learn other Struts classes that take part in the processing of Struts actions.

Note

In the RequestProcessor class, the moduleConfig variable references the ModuleConfig object for the current module.

The processMultipart Method

The first thing the request processor’s process method does is check if the current request is uploading a file/files. If this is the case, the processMultipart method (in Listing 22.3) wraps the current HttpServletRequest object in an org.apache.struts.upload.MultipartRequestWrapper object.

Listing 22.3: The processMultipart method

protected HttpServletRequest processMultipart(HttpServletRequest
  request) {
  if (!"POST".equalsIgnoreCase(request.getMethod())) {
    return (request);
  }

  String contentType = request.getContentType();
  if ((contentType != null) &&
    contentType.startsWith("multipart/form-data")) {
    return (new MultipartRequestWrapper(request));
  }
  else {
    return (request);
  }
}

The processPath method

After checking if the current request is uploading a file/files, the process method continues by obtaining the path of the invoked Struts action by calling the processPath method (given in Listing 22.4).

Listing 22.4: The processPath method

protected String processPath(HttpServletRequest request,
  HttpServletResponse response) throws IOException {
  String path = null;

  // For prefix matching, match on the path info (if any)
  path = (String) request.getAttribute(INCLUDE_PATH_INFO);
  if (path == null) {
    path = request.getPathInfo();
  }
  if ((path != null) && (path.length() > 0)) {
    return (path);
  }

  // For extension matching, strip the module prefix and extension
  path = (String) request.getAttribute(INCLUDE_SERVLET_PATH);
  if (path == null) {
    path = request.getServletPath();
  }
  String prefix = moduleConfig.getPrefix();
  if (!path.startsWith(prefix)) {
    String msg = getInternal().getMessage("processPath",
      request.getRequestURI());
    log.error(msg);
    response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    return null;
  }

  path = path.substring(prefix.length());
  int slash = path.lastIndexOf("/");
  int period = path.lastIndexOf(".");
  if ((period >= 0) && (period > slash)) {
    path = path.substring(0, period);
  }
  return (path);
}

As you can see in Listing 22.4, the action path is the same as the HttpServletRequest object’s path information. For instance, the path of a request having the following URL

http://localhost:8080/app22a/displayForm.do

is displayForm.

The processLocale Method

Struts supports internationalization and localization. It is therefore important to retrieve the user’s locale before further processing. The processLocale method in Listing 22.5 handles the user’s locale.

Listing 22.5: The processLocale Method

protected void processLocale(HttpServletRequest request,
  HttpServletResponse response) {

  // Are we configured to select the Locale automatically?
  if (!moduleConfig.getControllerConfig().getLocale()) {
    return;
  }

  // Has a Locale already been selected?
  HttpSession session = request.getSession();
  if (session.getAttribute(Globals.LOCALE_KEY) != null) {
    return;
  }

  // Use the Locale returned by the servlet container (if any)
  Locale locale = request.getLocale();
  if (locale != null) {
    if (log.isDebugEnabled()) {
      log.debug(" Setting user locale '" + locale + "'");
    }
    session.setAttribute(Globals.LOCALE_KEY, locale);
  }
}

In order to determine the correct locale, the processLocale method follows these steps.

  1. Checks the value of the locale property of the ControllerConfig object in the ModuleConfig object. This boolean value indicates whether or not the user locale should be stored in the HttpSession object and by default is true. You can change the value by assigning true or false to the locale property of the controller element in the Struts configuration file. If this value is false, processLocale returns immediately.
  2. Retrieve the user locale in the HttpSession object, if any.
  3. Obtain the locale from the getLocale method on the HttpServletRequest object.

The processContent Method

The processContent method (in Listing 22.6) sets the HttpServletResponse object’s content type with the value of the controller element’s contentType attribute in the Struts configuration file. By default, the value is text/html.

Listing 22.6: The processContent Method

protected void processContent(HttpServletRequest request,
  HttpServletResponse response) {
  String contentType =
    moduleConfig.getControllerConfig().getContentType();
  if (contentType != null) {
    response.setContentType(contentType);
  }
}

The processNoCache Method

The processNoCache method (in Listing 22.7) determines whether or not to disable caching by the browser. If the value of the controller element’s nocache attribute in the corresponding Struts configuration file is true, browser caching will be disabled by setting the Pragma and Cache-Control HTTP response headers to No-cache and no-cache,no-store,max-age=0, respectively. In addition, the Expires date header is set to 1 so that the content will expire immediately, hence prevent browser caching.

Listing 22.7: The processNoCache Method

protected void processNoCache(HttpServletRequest request,
  HttpServletResponse response) {
  if (moduleConfig.getControllerConfig().getNocache()) {
    response.setHeader("Pragma", "No-cache");
    response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
    response.setDateHeader("Expires", 1);
  }
}

The processPreprocess Method

The processPreprocess method in the RequestProcessor class does nothing, as you can see in Listing 22.8.

Listing 22.8: The processPreprocess Method

protected boolean processPreprocess(HttpServletRequest request,
  HttpServletResponse response) {
  return (true);
}

This method is meant to be overridden in a subclass of RequestProcessor and acts as a hook that you can program to perform some pre-processing tasks.

The processCachedMessages Method

The processCachedMessages method (given in Listing 22.9) removes any ActionMessages object cached in the HttpSession object if the object’s isAccessed method returns true.

Listing 22.9: The processCachedMessages Method

protected void processCachedMessages(HttpServletRequest request,
  HttpServletResponse response) {
  HttpSession session = request.getSession(false);
  if (session == null) {
    return;
  }
  ActionMessages messages =
    (ActionMessages) session.getAttribute(Globals.MESSAGE_KEY);
  if (messages == null) {
    return;
  }
  if (messages.isAccessed()) {
    session.removeAttribute(Globals.MESSAGE_KEY);
  }
}

The processMapping Method

Before studying the processMapping method, you should first understand the following classes: ActionConfig, ForwardConfig, ActionForward, ActionMapping.

The org.apache.struts.config.ActionConfig object is created for every action element in the Struts configuration file, and therefore each attribute of the action element corresponds to a property in the ActionConfig class. As such, the ActionConfig class has the following properties that correspond to the attributes with identical names in the action element: type, input, forward, validate, include, roles, scope, etc. An action element can also contain exception and forward subelements. Therefore, the ActionConfig class has a HashMap named forwards that contains all information pertaining to the forward subelements and another HashMap named exceptions for all exception subelements.

The org.apache.struts.config.ForwardConfig class represents a forward subelement nested inside an action element or under the global-forwards element. Each of the ForwardConfig class’s properties (name, path, redirect, contextRelative, className) corresponds to an attribute in a forward element.

The org.apache.struts.action.ActionForward class extends ForwardConfig and is very similar to the latter. ActionForward would have been deprecated if not for the fact that it is part of the API being used by existing applications.

The org.apache.struts.action.ActionMapping class extends ActionConfig and adds the following three methods:

The ActionMapping class is of utmost importance because an Action object can get an ActionForward object for its forward destination. In fact, the processMapping method called by the RequestProcessor class’s process method returns an ActionMapping for the current Struts action.

The processMapping method is given in Listing 22.10.

Listing 22.10: The processMapping Method

protected ActionMapping processMapping(HttpServletRequest request,
  HttpServletResponse response, String path) throws IOException {

  // Is there a mapping for this path?
  ActionMapping mapping = (ActionMapping)
    moduleConfig.findActionConfig(path);

  // If a mapping is found, put it in the request and return it
  if (mapping != null) {
    request.setAttribute(Globals.MAPPING_KEY, mapping);
    return (mapping);
  }

  // Locate the mapping for unknown paths (if any)
  ActionConfig configs[] = moduleConfig.findActionConfigs();
  for (int i = 0; i < configs.length; i++) {
    if (configs[i].getUnknown()) {
      mapping = (ActionMapping) configs[i];
      request.setAttribute(Globals.MAPPING_KEY, mapping);
      return (mapping);
    }
  }

  // No mapping can be found to process this request
  String msg = getInternal().getMessage("processInvalid", path);
  log.error(msg);
  response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
  return null;
}

The processRoles Method

The processRoles method, in Listing 22.11, makes sure that the user invoking the current action belongs to a role that has access to the current action. The process method returns immediately if the processRoles method returns false.

Listing 22.11: The processRoles Method

protected boolean processRoles(HttpServletRequest request,
  HttpServletResponse response, ActionMapping mapping)
  throws IOException, ServletException {

  // Is this action protected by role requirements?
  String roles[] = mapping.getRoleNames();
  if ((roles == null) || (roles.length < 1)) {
    return (true);
  }

  // Check the current user against the list of required roles
  for (int i = 0; i < roles.length; i++) {
    if (request.isUserInRole(roles[i])) {
      if (log.isDebugEnabled()) {
        log.debug(" User '" + request.getRemoteUser() +
          "' has role '" + roles[i] + "', granting access");
      }
      return (true);
    }
  }

  // The current user is not authorized for this action
  if (log.isDebugEnabled()) {
    log.debug(" User '" + request.getRemoteUser() +
      "' does not have any required role, denying access");
  }

  response.sendError(HttpServletResponse.SC_FORBIDDEN,
    getInternal().getMessage("notAuthorized", mapping.getPath()));
  return (false);
}

The processActionForm Method

The processActionForm method (Listing 22.12) returns an action form for an action that requires one, i.e. if the corresponding action element’s input attribute is present. The retrieved ActionForm object is also stored in the scoped specified by the scope attribute of the action element.

Listing 22.12: The processActionForm Method

protected ActionForm processActionForm(HttpServletRequest request,
  HttpServletResponse response, ActionMapping mapping) {

  // Create (if necessary) a form bean to use
  ActionForm instance = RequestUtils.createActionForm
    (request, mapping, moduleConfig, servlet);
  if (instance == null) {
    return (null);
  }

  // Store the new instance in the appropriate scope
  if (log.isDebugEnabled()) {
    log.debug(" Storing ActionForm bean instance in scope '" +
    mapping.getScope() + "' under attribute key '" +
    mapping.getAttribute() + "'");
  }
  if ("request".equals(mapping.getScope())) {
    request.setAttribute(mapping.getAttribute(), instance);
  }
  else {
    HttpSession session = request.getSession();
    session.setAttribute(mapping.getAttribute(), instance);
  }
  return (instance);
}

The processActionForm method calls the createActionForm method of the utility class org.apache.struts.util.RequestUtils. The logical name of the action form to be retrieved is obtained by calling the getName method of the ActionMapping object that is passed as the second argument to the createActionForm method.

The processPopulate Method

The processPopulate method populates the action form returned by the processActionForm method with the current HttpServletRequest object’s request parameters. This method is given in Listing 22.13.

Listing 22.13: The processPopulate Method

protected void processPopulate(HttpServletRequest request,
  HttpServletResponse response, ActionForm form,
  ActionMapping mapping) throws ServletException {

  if (form == null) {
    return;
  }

  // Populate the bean properties of this ActionForm instance
  if (log.isDebugEnabled()) {
    log.debug(" Populating bean properties from this request");
  }

  form.setServlet(this.servlet);
  form.reset(mapping, request);

  if (mapping.getMultipartClass() != null) {
    request.setAttribute(Globals.MULTIPART_KEY,
      mapping.getMultipartClass());
  }

  RequestUtils.populate(form, mapping.getPrefix(), mapping.getSuffix(),
    request);

  // Set the cancellation request attribute if appropriate
  if ((request.getParameter(Constants.CANCEL_PROPERTY) != null) ||
    (request.getParameter(Constants.CANCEL_PROPERTY_X) != null)) {
    request.setAttribute(Globals.CANCEL_KEY, Boolean.TRUE);
  }
}

The processValidate Method

The processValidate method calls the validate method of the action form returned by the processActionForm method. The method returns true if the validation was a success and this causes the process method to proceed with the next line.

If validation failed, the processValidate method checks if the corresponding action’s input attribute has a value. If the input attribute is not present, the processValidate method sends an error message to the browser. If the input attribute can be found, the ActionErrors collection returned by the action form’s validate method is saved in the HttpServletRequest object and the processForwardConfig method is called.

The processValidate method is given in Listing 22.14.

Listing 22.14: The processValidate Method

protected boolean processValidate(HttpServletRequest request,
  HttpServletResponse response, ActionForm form,
  ActionMapping mapping) throws IOException, ServletException {

  if (form == null) {
    return (true);
  }
  // Was this request cancelled?
  if (request.getAttribute(Globals.CANCEL_KEY) != null) {
    if (log.isDebugEnabled()) {
      log.debug(" Cancelled transaction, skipping validation");
    }
    return (true);
  }

  // Has validation been turned off for this mapping?
  if (!mapping.getValidate()) {
    return (true);
  }

  // Call the form bean's validation method
  if (log.isDebugEnabled()) {
    log.debug(" Validating input form properties");
  }
  ActionMessages errors = form.validate(mapping, request);
  if ((errors == null) || errors.isEmpty()) {
    if (log.isTraceEnabled()) {
      log.trace("  No errors detected, accepting input");
    }
    return (true);
  }

  // Special handling for multipart request
  if (form.getMultipartRequestHandler() != null) {
    if (log.isTraceEnabled()) {
      log.trace("  Rolling back multipart request");
    }
    form.getMultipartRequestHandler().rollback();
  }

  // Was an input path (or forward) specified for this mapping?
  String input = mapping.getInput();
  if (input == null) {
    if (log.isTraceEnabled()) {
      log.trace("  Validation failed but no input form available");
    }
    response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
      getInternal().getMessage("noInput", mapping.getPath()));
    return (false);
  }

  // Save our error messages and return to the input form if possible
  if (log.isDebugEnabled()) {
    log.debug(" Validation failed, returning to '" + input + "'");
  }
  request.setAttribute(Globals.ERROR_KEY, errors);
  if (moduleConfig.getControllerConfig().getInputForward()) {
    ForwardConfig forward = mapping.findForward(input);
    processForwardConfig( request, response, forward);
  }
  else {
    internalModuleRelativeForward(input, request, response);
  }
  return (false);
}

The processForward Method

The processForward method (in Listing 22.15) checks if the current action element’s forward attribute is present. If it is present, the processForward method forwards control to the destination specified by the forward attribute. Otherwise, the processForward method returns true to indicate that the process method should continue with the next line.

Listing 22.15: The processForward Method

protected boolean processForward(HttpServletRequest request,
  HttpServletResponse response, ActionMapping mapping)
  throws IOException, ServletException {

  String forward = mapping.getForward();
  if (forward == null) {
    return (true);
  }
  internalModuleRelativeForward(forward, request, response);
  return (false);
}

The processInclude Method

This method checks if the current action element’s include attribute is present. If so, the processInclude method includes the specified resource. Otherwise, it returns true to indicate that the process method should continue processing.

The processInclude method is given in Listing 22.16.

Listing 22.16: The processInclude Method

protected boolean processInclude(HttpServletRequest request,
  HttpServletResponse response, ActionMapping mapping)
  throws IOException, ServletException {

  String include = mapping.getInclude();
  if (include == null) {
    return (true);
  }
  internalModuleRelativeInclude(include, request, response);
  return (false);
}

The processActionCreate Method

This method returns the appropriate Action object associated with the current action element. The processActionCreate method is given in Listing 22.17

Listing 22.17: The processActionCreate Method

protected Action processActionCreate(HttpServletRequest request,
  HttpServletResponse response, ActionMapping mapping)
  throws IOException {

  // Acquire the Action instance we will be using (if there is one)
  String className = mapping.getType();
  if (log.isDebugEnabled()) {
    log.debug(" Looking for Action instance for class " + className);
  }

  Action instance = null;
  synchronized (actions) {
    // Return any existing Action instance of this class
    instance = (Action) actions.get(className);
    if (instance != null) {
      if (log.isTraceEnabled()) {
        log.trace("  Returning existing Action instance");
      }
      return (instance);
    }

    // Create and return a new Action instance
    if (log.isTraceEnabled()) {
      log.trace("  Creating new Action instance");
    }
    try {
      instance = (Action) RequestUtils.applicationInstance(className);
    }
    catch (Exception e) {
      log.error(getInternal().getMessage("actionCreate",
        mapping.getPath()), e);
      response.sendError(
        HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
        getInternal().getMessage("actionCreate", mapping.getPath()));
      return (null);
    }

    instance.setServlet(this.servlet);
    actions.put(className, instance);
  }

  return (instance);
}

The processActionPerform Method

The processActionPerform method (in Listing 22.18) invokes the execute method on the Action object returned by the processActionCreate method. This in effect runs the business logic in the Action object.

Listing 22.18: The processActionPerform Method

protected ActionForward processActionPerform(HttpServletRequest
  request, HttpServletResponse response, Action action, ActionForm
  form, ActionMapping mapping) throws IOException, ServletException {
  try {
    return (action.execute(mapping, form, request, response));
  }
  catch (Exception e) {
    return (processException(request, response, e, form, mapping));
  }
}

The processForwardConfig Method

The processForwardConfig method forwards or redirects the user to the specified destination. This method is given in Listing 22.19.

Listing 22.19: The processForwardConfig Method

protected void processForwardConfig(HttpServletRequest request,
  HttpServletResponse response, ForwardConfig forward) throws
  IOException, ServletException {

  if (forward == null) {
    return;
  }

  if (log.isDebugEnabled()) {
    log.debug("processForwardConfig(" + forward + ")");
  }

  String forwardPath = forward.getPath();
  String uri = null;

  // paths not starting with / should be passed through without any
  // processing (ie. they're absolute)
  if (forwardPath.startsWith("/")) {
    uri = RequestUtils.forwardURL(request, forward, null);
    // get module relative uri
  }
  else {
    uri = forwardPath;
  }

  if (forward.getRedirect()) {
    // only prepend context path for relative uri
    if (uri.startsWith("/")) {
      uri = request.getContextPath() + uri;
    }
    response.sendRedirect(response.encodeRedirectURL(uri));
  }
  else {
    doForward(uri, request, response);
  }
}

Previous
Next