| This Chapter | |
| - | Chapter 22: How Struts Works |
| - | The Action Servlet |
| - | Creating A ModuleConfig Object |
| - | The Request Processor |
| - | Summary |
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 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);
}
}
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.
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.
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 (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 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 (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);
}
}
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, 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 (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 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 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 (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);
}
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);
}
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 (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 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);
}
}