Skip navigation

Category Archives: Struts Tips



Handle Exception two ways in Web app

Discussion
In J2ee web application, there are two types of errors in the Web Tier:


  • HTTP Errors (400 – 500 series response)
    Communicate problems to Web clients.
  • Java Exceptions
    Communicate problems to Web container.



Exceptions can be generated within the tier or passed back from other tiers.
If you want exceptions to be passed on to a client, you must convert them to HTTP errors.



Exceptions thrown to the client

  • javax.servlet.ServletException
  • javax.io.IOException


In J2ee Web application, javax.servlet.ServletException and java.io.IOException will be thrown to the client.
All the exceptions will be wrapped into ServletException and thrown to the client, if you do not handle them by yourself.
So, we decide handling exceptions two ways:

  1. Handle Exceptions which are thrown under Action tier using ExceptionHandler in Struts (see post: Exception Handling in Struts)
  2. Handle Exceptions which are thrown over Action tier by configurating web.xml using tag <error-page>


approach 2:

In web.xml


<error-page>
    <exception-type>javax.servlet.ServletException</exception-type>
    <location>/error.do</location>
</error-page>
<error-page>
    <exception-type>javax.io.IOException</exception-type>
    <location>/error.do</location>
</error-page>


Define ErrorAction to handle exceptions


public class ErrorAction extends Action {
     /**
     * The request attribute under which we 
     * forward an HTTP status message
     * (as an object of type STring) to an error page.
     */
    public static final String ERROR_MESSAGE_ATTR =
        "javax.servlet.error.message";
    /**
     * The request attribute under which we 
     * forward a Java exception type
     * (as an object of type Class) to an error page.
     */
    public static final String EXCEPTION_TYPE_ATTR =
        "javax.servlet.error.exception_type";
    /**
     * The request attribute under which we 
     * forward the request URI
     * (as an object of type String) of the page on which 
     * an error occurred.
     */
    public static final String EXCEPTION_PAGE_ATTR =
        "javax.servlet.error.request_uri";
    /**
     * The request attribute under which we 
     * forward a Java exception
     * (as an object of type Throwable) to an error page.
     */
    public static final String EXCEPTION_ATTR =
        "javax.servlet.error.exception";
    /**
     * The request attribute under which we 
     * forward an HTTP status code
     * (as an object of type Integer) to an error page.
     */
    public static final String STATUS_CODE_ATTR =
        "javax.servlet.error.status_code";

    public ActionForward execute(
            ActionMapping mapping, 
            ActionForm form,
            HttpServletRequest request, 
            HttpServletResponse response)
            throws Exception {
        // ErrorMessage (java.lang.String)
        Object errorMsg = 
            request.getAttribute(ERROR_MESSAGE_ATTR);
        // Exception (java.lang.Exception)
        Object exception = 
            request.getAttribute(EXCEPTION_ATTR);
        // Exception Class (java.lang.Class)
        Object exceptionType = 
            request.getAttribute(EXCEPTION_TYPE_ATTR);
        // uri (java.lang.String)
        Object page = 
            request.getAttribute(EXCEPTION_PAGE_ATTR);
        // Error Code (java.lang.Integer)
        Object errorCode = 
            request.getAttribute(STATUS_CODE_ATTR);
        
        ActionErrors errors = 
            (ActionErrors) request.getAttribute(
                Globals.ERROR_KEY);
        
        if(errors == null) {
            errors = new ActionErrors();
        }
        errors.add("innerError", 
            new ActionMessage("errors.inner.error", 
                errorMsg, exception, errorCode, page));
        saveErrors(request, errors);
        return mapping.findForward("toErrorPage");
    }
}


In struts-config.xml, define ErrorAction


<action
    path="/error"
    type="org.springframework.web.struts.DelegatingActionProxy"
    scope="request">
        <forward name="toErrorPage" path="/error_page.jsp"/>
</action>


MessageResources.properties:


errors.inner.error=Error Message: {0} 
<br />Error Type : {1} <br />Error Code : {2} 
<br />Request URI : {3}


Jsp File


<span class=”errorMsg”><html:errors /></span>


Doing so, and handling exception in struts using ExceptionHandler,
you can handle the exceptions thrown not only under Action tier, but also over the Action tier.



Exception Handling in Struts

Discussion
Exception is hard to aviod in our application. So we just catch it, log it, show error messages in Error Page.
There are lots of appoach to do this. Now I just introduce one of them.
Extending Struts’ ExceptionHandler and using <global-exceptions> tag in struts-config.xml

The following is the sample code:


public class MyExceptionHandler extends ExceptionHandler {
    public ActionForward execute(
            Exception ex, 
            ExceptionConfig ae,
            ActionMapping mapping, 
            ActionForm formInstance,
            HttpServletRequest request, 
            HttpServletResponse response)
            throws ServletException {
        ActionForward forward = null;
        ActionErrors errors = 
            (ActionErrors) request.getAttribute(
                Globals.ERROR_KEY);
        if(ex instanceof PdfGenerateException) {
            if(errors == null) {
                errors = new ActionErrors();
            }
            errors.add(new ActionMessage(ae.getKey()));
        }
        if(ex instanceof WrongQueryStringException) {
            if(errors == null) {
                errors = new ActionErrors();
            }
            errors.add(new ActionMessage(ae.getKey()));
        }
        if(errors != null && !errors.isEmpty()) {
            request.setAttribute(Globals.ERROR_KEY, errors);
        }
        if (ae.getPath() != null) {
            forward = new ActionForward(ae.getPath());
        } else {
            forward = 
                super.execute(
                    ex, ae, mapping, formInstance, request, response);
        }
        return forward;
    }
}

In struts-config.xml, define <global-exceptions>


<global-exceptions>
    <exception
        key="errors.generate.pdf" 
        handler="jp.co.gky.handler.MyExceptionHandler"
        path="/error.do"
        type="jp.co.gky.exception.PdfGenerateException" />
    <exception
        key="errors.request.error" 
        handler="jp.co.gky.handler.MyExceptionHandler"
        path="/error.do"
        type="jp.co.gky.exception.WrongQueryStringException" />
</global-exceptions>

MessageResources.properties:


errors.request.error=QueryString "pageNo" is not a integer.
errors.generate.pdf=Exception occured when generating pdf file.
errors.illegal.argument=Illegal Argument {0}

Jsp File

<span class=”errorMsg”><html:errors /></span>



Implement download file

Discussion
Sometimes maybe the business logic needs to generate some *.csv, *.pdf, *.txt, etc to be available for user downloading.
Struts 1.2x, supports DownloadAction to implement downloading file.
struts-extras-1.2.x.jar must be added in classpath.
DownloadAction is an abstract class, and you should override


protected StreamInfo getStreamInfo( 
    ActionMapping mapping, 
    ActionForm form, 
    HttpServletRequest request, 
    HttpServletResponse response) 
    throws Exception

method.

In DownloadAction, there is an inner interface StreamInfo and two implement inner class FileStreamInfo and ResourceStreamInfo.
The FileStreamInfo simplifies the downloading of a file from the disk. and ResourceStreamInfo simplifies the downloading of a web application resource.

When overriding getStreamInfo() method, just set ContentType, ServletContext, web application resource’s path and response header if using ResourceStreamInfo as
StreamInfo’s implement class.
If using FileStreamInfo, just set ContentType, java.io.File and response’s header, when overriding getStreamInfo() method.

The following is the sample code:

CsvDownloadAction (using ResourceStreamInfo)


public class CsvDownloadAction 
  extends DownloadAction { 
  protected StreamInfo getStreamInfo( 
      ActionMapping mapping, 
      ActionForm form, 
      HttpServletRequest request, 
      HttpServletResponse response) 
      throws Exception { 
      ServletContext servletCtx = 
        request.getSession(false).getServletContext(); 
      String contentType = "application/octet-stream"; 
      String fileName = "abc.csv"; 
      String path="WEB-INF/csv/" + fileName; 
      response.setHeader( 
        "Content-Disposition", 
        "attachment;filename=" + fileName); 
      return 
        new ResourceStreamInfo( 
          contentType, servletCtx, path); 
  } 
}

CsvDownloadAction (using FileStreamInfo)


public class CsvDownloadAction 
  extends DownloadAction { 
  protected StreamInfo getStreamInfo( 
      ActionMapping mapping, 
      ActionForm form, 
      HttpServletRequest request, 
      HttpServletResponse response) 
      throws Exception { 
      ServletContext servletCtx = 
        request.getSession(false).getServletContext(); 
      String contentType = "application/octet-stream"; 
      String fileName = "abc.csv"; 
      String path = servletCtx.getRealPath("/") +
         "WEB-INF/csv/" + fileName;
      response.setHeader( 
        "Content-Disposition", 
        "attachment;filename=" + fileName); 
      return 
        new FileStreamInfo(contentType, new File(path));
  } 
}

Jsp File

<html:link: path=”/csvDownload.do”>abc.csv</html:link>




Get Real Path from ServletContext


Discussion
Sometimes your web application will generate some file into your physisical disk, such as *.pdf, *.xls, *.txt, …
according to your business requirement. So your application has to know the real path to generate the requested
files. If you hard code the real path in some configuration file to do this, OK, no problem. But sometimes maybe
you have no right to know the real path where your web application is residing. Such as if you rent a sharing host
of another host renting company. And the administrator won’t tell you the real path information.
On another hand, if you hard code the real path, it will not be a good choice when imigrating your web application
I think. You have to change your configuration file each time when you change a running server.
So, here is the problem, how to do?
Thank God, the J2EE support you ServletContext, and we can fix this up.
ServletContext supports us a method called getRealPath(String path). If a virtual path as the path parameter, the return
value is the real path for the given virtual path.

Here is the example:

The web application residing real path: C:\jakarta-tomcat-5.0.28\webapps\dispatchEx\
The dispatchEx is the web application. The application will generate a pdf file everytime when the user clicks
the link in the web page. And the file will be generate under the real path of this web application.

Solution

public class GeneratePdfAction extends Action {
  public ActionForward execute(ApplicationMapping mapping, ActionForm form,
          HttpServletRequest request, HttpServletResponse response) throws Exception {
    String dir = 
        getServlet().getServletContext().getRealPath("/");
    //dir=C:\jakarta-tomcat-5.0.28\
            webapps\dispatchEx\ while debuging
    generatePdf(dir);
    return mapping.findforward("success");

  }
  private void generatePdf(String dir) {
    String filename = dir + 
      DateFormatUtils.format(System.currentTimeMillis(), 
                                           "yyyyMMddHHmm") +
          RandomStringUtils.random(4, true, false) + ".pdf";
    this.pdfGenerator.generatePdf(filename);
  }
  public PdfGenerator getPdfGenerator() {
    return this.pdfGenerator
  }
  public void setPdfGenerator(PdfGenerator pdfGenerator) {
    this.pdfGenerator = pdfGenerator;
  }
}




Using Struts EventDispatchAction


Discussion
When using more than two

<input type="image" src="image/submit_btn.gif" border="0" alt="Submit"/>


tags as submit button to submit form data to request, Struts 1.1’s LookupDispatchAction doesn’t work.

for example:

<!DOCTYPE HTML 
  PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<%@ page 
  contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<html>
<head>
 <title>EventDispatchAction Example</title>
  <META http-equiv="Content-Type" content="text/html; 
  charset=UTF-8"/>
  <link href="css/style.css" 
  rel="stylesheet" type="text/css" >
</head>
<body>
<html:form action="/gkyEventDispatch">
<TABLE border="0" cellspacing="0" cellpadding="0" width="700">
  <TR>
    <TD>Name</TD>
    <TD>
      <html:text property="name" styleClass="textInput"/>
    </TD>
  </TR>
  <TR>
    <TD>
      <html:image property="search" 
        src="image/submit_search_btn.gif" 
        border="0"/>
    </TD>
    <TD>
      <html:image property="add" 
        src="image/submit_add_btn.gif" 
        border="0"/>
      </TD>
  </TR>
</TABLE>
</html:form>
</body>
</html>

Solution

Using EventDispatchAction instead of LookupDispatchAction to implement one form multi-image-submit button.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.3//EN"
  "http://struts.apache.org/dtds/struts-config_1_3.dtd" >
<struts-config>
  <form-beans>
    <form-bean name="gkyForm" type="jp.co.gky.form.GkyForm"/>
  </form-beans>
  <action-mappings>
    <action
        path="/gkyInit"
        type="jp.co.gky.action.GkyInitAction"
        name="gkyForm"
        scope="request">
      <forward name="gkyInitOk" path="/gkyInput.jsp"/>
    </action>
    <action
        path="/gkyEventDispatch"
        type="jp.co.gky.action.GkyEventDispatchAction"
        name="gkyForm"
        input="/gkyInput.jsp"
        validate="false"
        parameter="add=doAdd,search=doSearch"
        scope="request">
      <forward redirect="false" 
          name="doAddOk" path="/gkyInit.do"/>
      <forward redirect="false" 
          name="doSearchOk" path="/gkyInit.do"/>
    </action>
  </action-mappings>
  <message-resources parameter="MessageResource"/>
 
</struts-config>

public class GkyEventDispatchAction 
                        extends EventDispatchAction {
  public ActionForward doAdd(
      ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response) 
      throws Exception {
    ActionErrors errors = new ActionErrors();
    if(!isTokenValid(request)) {
      errors.add(ActionMessages.GLOBAL_MESSAGE, 
          new ActionMessage("errors.token"));
    }
    resetToken(request);
    if(!errors.isEmpty()) {
      saveErrors(request, errors);
      saveToken(request);
      return mapping.getInputForward();
    }
    // .....
    request.setAttribute("newKeyValue", reqMsg);
    return mapping.findForward("doAddOk");
  }

  public ActionForward doSearch(
      ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws Exception {
    ActionErrors errors = new ActionErrors();
    if(!isTokenValid(request)) {
      errors.add(ActionMessages.GLOBAL_MESSAGE, 
          new ActionMessage("errors.token"));
    }
    resetToken(request);
    if(!errors.isEmpty()) {
      saveErrors(request, errors);
      saveToken(request);
      return mapping.getInputForward();
    }
    //....
    request.setAttribute("searchKeyValue", reqMsg);
    return mapping.findForward("doSearchOk");
  }
}




Avoid Form Data Duplicate submitting


Discussion
Form data duplicate submitting can caused by either:
1.Using the browser BACK button to return to the previous page and resubmit the form.2.Refreshing this page and selecting OK when the broswer ask whether you want to resubmit the form data.

Solution

Using Token.

The following is the sample code:

public class InitAction extends Action { 
  public ActionForward execute( 
    ActionMapping mapping, ActionForm form, 
      HttpServletRequest request, HttpServletResponse response) 
      throws Exception { 
    GkyForm gkyForm = (GkyForm) form; 
    gkyForm.setKey(""); 
    gkyForm.setValue(""); 
    // avoid duplicated submitting to set Token here. 
    saveToken(request); 
    return mapping.findForward("gkyInitOk"); 
  } 
}  

public class SubmitAction extends Action { 
  public ActionForward execute( 
    ActionMapping mapping, ActionForm form, 
      HttpServletRequest request, HttpServletResponse response) 
      throws Exception { 
    ActionErrors errors = new ActionErrors(); 
    if(!isTokenValid(request)) { 
      errors.add(ActionMessages.GLOBAL_MESSAGE, 
        new ActionMessage("errors.token")); 
    } 
    resetToken(request); 
    if(!errors.isEmpty()) { 
      saveErrors(request, errors); 
      saveToken(request); 
      return mapping.getInputForward();  
    } 
    GkyForm gkyForm = (GkyForm) form; 
    String key = gkyForm.getKey(); 
    String value = gkyForm.getValue(); 
    // ..... 
    return mapping.findForward("doAddOk"); 
  } 
}

The following is Processing Flow

processing-flow.gif



Defeating Browser Caching

Discussion
To speed processing, browsers frequently keep a copy of a visited page on the client’s local system. If an identical URL for the original page is requested and that page hasn’t expired, the browser may display the page from the local cache instead of issuing a new request. This caching reduces network traffic and improves the user experience significantly. However, this can cause problems for dynamically generated pages. Consider a JSP page that renders data retrieved from the HTTP session.If data stored in the session changes, the browser won’t be aware of the change.When the browser receives a new request for the page, it serves up the old page instead.
Solution

[1] general solution:in struts-config.xml:

<controller nocache="true"/>

[2] per-action basis solution:

Customize action-mapping for avoiding browser caching

public class NocacheActionMapping extends ActionMapping { 
  private String nocache; 
  private boolean nocacheEnabled = false;   

  public String getNocache() { 
    return this.nocache; 
  } 
  public void setNocache(String nocache) { 
    this.nocache = nocache; 
    this.nocacheEnabled = new Boolean(nocache).booleanValue(); 
  } 
  public boolean isNocacheEnabled() { 
    return this.nocacheEnabled; 
  } 
}

Customize RequestProcessor

public class NocacheRequestProcessor extends RequestProcessor { 
  protected ActionForward processActionPerform( 
                    HttpServletRequest request, 
                    HttpServletResponse response, 
                    Action action, 
                    ActionForm form, 
                    ActionMapping mapping) 
                    throw IOException, ServletException { 
    ActionForward forward = null; 
    if(mapping instanceof NocacheActionMapping) { 
      NocacheActionMapping nocacheMapping = 
                (NocacheActionMapping) mapping; 
      if(nocacheMapping.isNocacheEnabled()) { 
        response.setHeader("Pragma", "No-cache"); 
        response.setHeader("Cache-Control", "no-cache"); 
        response.setDateHeader("Expires", 0); 
      } 
    } 
    forward = 
        super.processActionPerform( 
                  request, response, action, form, mapping); 
    return forward; 
  } 
}

Struts-config for action-based response caching


<!DOCTYPE struts-config PUBLIC 
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" 
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> 
<struts-config> 
  <action-mappings 
      type="com.xxx.common.actionMapping.NocacheActionMapping"> 
    <action path="/xCustomized" 
        type="com.xxx.action.CustomizedAction" 
        name="customizedForm" 
        scope="request"> 
      <set-property property="nocache" value="true"/> 
      <forward name="success" path="/customizedPage.jsp"/> 
    </action> 
  </action-mappings> 
  <controller 
    processorClass="com.xxx.ctrl.NocacheRequestProcessor"/> 
</struts-config>