X

Geertjan's Blog

  • November 12, 2005

Struts for the Complete Beginner (Part 2)

Geertjan Wielenga
Product Manager
Building on yesterday's quick introduction to Struts, I added a password field to the ActionForm Bean class and added it to the table in loginForm.jsp. Then I used the IDE to enhance the application with a lot of Struts functionality. The sections that follow show how to enhance the application by adding the following:
  • Validation functionality.

    Syntax-level: Enhance the ActionForm Bean class to check whether the name and password fields in the loginForm.jsp are filled in. If they are empty, produce error messages that the ActionForm Bean class obtains from the ApplicationResource.properties file.

    Business-level: Add a SecurityManager class and use it in the Action class to check whether the name and password entered in loginForm.jsp are correct. If one is incorrect, produce an error message that the Action class obtains from the ApplicationResource.properties file.
  • Cancel functionality. Add a 'Cancel' button to loginForm.jsp. Add the org.apache.struts.action.Action.isCancelled method so that, when the Cancel button is clicked, a new JSP page is called via struts-config.xml.
  • Logout functionality. Add a 'Logout' link to loginSuccessful.jsp that, when clicked, calls a new JSP page via struts-config.xml.
  • Error handling functionality. Rewrite the code for the 'Cancel' button so that, when it is clicked, an error is produced and a new JSP page is called via struts-config.xml. The new JSP page displays an error message that it obtains from the ApplicationResource.properties file.

Visually, this is how the application looks after the functionality listed above is added (note that all the screenshots below can be enlarged by clicking on them)—the first thing you see is loginForm.jsp, with error messages generated by the ActionForm Bean class. Actually, I'd prefer the ActionForm Bean class's validate method to be called only after I click the Login button, so if someone knows how to set that up, please let me know. I've worked around this problem by prepending 'Hint' before the error message, so that the user doesn't end up seeing the word 'error' before even having done anything in the application. So, this is what the first page currently looks like:


So, because the two fields are empty, the ActionForm Bean class gets two error messages via the ApplicationResource.properties file. Then, if you type in only one of the two (as shown below), you still get an error message for the other empty field:


Next, even if both fields are filled in, you get a new error message if the name and password are incorrect (as shown below):


Only if the name and password are admin/admin (note that currently the password is unencrypted), as shown here...


...will you get loginSuccessful.jsp:


Then, when you click the Logout link, loginOut.jsp is displayed:


Finally, we rewrite the Cancel button so that a java.lang.RuntimeException is thrown when the Cancel button is clicked. Overriding the isCancelled method in the Action class calls a new JSP page that displays an error message obtained from the ApplicationResource.properties file:


So, to add all of the above functionality, do the following (in any order):

  • Add Syntax-Level Validation Functionality. In Struts, the ActionForm Bean acts as a bridge between a JSP page and a Struts action. It captures user input from the JSP pages and delivers it to the action. An ActionForm Bean can also validate the input before passing it on to the action. So, here's how the ActionForm Bean checks whether the fields are empty or not (by the way, the first field, name, is created and validated by default when you use the New ActionForm Bean wizard in the IDE):

    public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
    ActionErrors errors = new ActionErrors();
    if (getName() == null || getName().length() < 1) {
    errors.add("nameEmpty", new ActionMessage("error.name.required"));
    }
    if (getPassword() == null || getPassword().length() < 1) {
    errors.add("passwordEmpty", new ActionMessage("error.password.required"));
    }
    return errors;
    }

    The ApplicationResource.properties file (which the IDE creates for you when you add Struts to a project) contains all the display messages. Two display messages are created above, their display text is found by Struts in the ApplicationResource.properties file, and displayed in LoginForm.jsp. So, add the following to ApplicationResource.properties file:

    error.name.required=Type your name in the Name field.
    error.password.required=Type your password in the Password field.

    And this is how I display them in loginForm.jsp:

    <html:errors property="nameEmpty" />
    <html:errors property="passwordEmpty" />

    To use the above <html:errors> tags, you need to have the following taglib directive at the top of loginForm.jsp:

    <%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %>

  • Add Business-Level Validation Functionality. For the first time, I now really understand the difference between syntax-level and business-level validation. Ask yourself this: Would the business for which this application is created care if the fields in the login form are empty? I strongly doubt it. But would they care if the user was able to log in to the application without being authorized? Yes, obviously the business would care. Helpfully, Struts makes a nice split between syntax-level and business-level validation: the former is done by the ActionForm Bean class while the latter is done by the Action class. Since the ActionForm Bean class acts as a bridge between the JSP page and the action, you want syntax validation to occur in the ActionForm Bean class, because this class deals directly with the JSP page, while the action does not. The action is more concerned with business-level issues.

    So, I have a very simple class called SecurityManager:

    package com.myapp.struts;
    public class SecurityManager {
    /\*\* Creates a new instance of SecurityManager \*/
    public SecurityManager() {
    }
    public static boolean AuthenticateUser(String name, String password) {
    // Business level authentication - simple check of user name/password = admin/admin
    // TODO: Implement authentication (e.g. use database, etc.)
    return name.equals("admin") && password.equals("admin");
    }
    }

    And in the execute method in the Action class I have this:

    if (SecurityManager.AuthenticateUser(newStrutsActionForm.getName(),newStrutsActionForm.getPassword())){
    return mapping.findForward(SUCCESS);
    } else {
    return mapping.getInputForward();
    }

    Notice that the name and password are retrieved from the ActionForm Bean class and authenticated against the SecurityManager class. To be able to use the ActionForm Bean class, you need to cast it to the correct type. Do this at the top of the Action class's execute method, like this:

    NewStrutsActionForm newStrutsActionForm = (NewStrutsActionForm)form;

    So, if the correct name and password are not retrieved by the ActionForm Bean class, loginForm.jsp is displayed again (that's what getInputForward() does). However, the user doesn't know what's wrong, because you haven't displayed an appropriate error message.

    Add the following right above the return mapping.getInputForward() line:

    ActionMessages errors = new ActionMessages();
    ActionMessage error = new ActionMessage("errors.login.invalid");
    errors.add("loginWrong",error);
    saveErrors(request.getSession(),errors);

    Here you see a new error message is created. So go to the ApplicationResource.properties file and define the display text for the errors.login.invalid error:

    errors.login.invalid=Wrong name or password. Try again.

    And then display it in loginForm.jsp, just like the earlier error messages:

    <html:errors property="loginWrong" />
  • Add Cancel Functionality. Adding a Cancel button is a piece of cake in Struts. Here you see mine, right below the submit button (it is highlighted in the screenshot):

    But how to specify what happens when the button is clicked? Add this to the execute method in the Action class:

    if (isCancelled(request)){
    return mapping.findForward(CANCEL);
    }

    Helpfully, Struts Javadoc in the IDE tells you what the above code does:

    Now you need to map that variable CANCEL to something. At the top of the Action class, add the following line below the SUCCESS line:

    private final static String CANCEL = "cancel";

    So, what should happen when the 'Cancel' button is clicked? Add a JSP page called loginCancel.jsp and change the default text between the H1 tags to Login Cancelled!. Now go to struts-config.xml, right-click anywhere, choose Struts and then choose Add Forward:

    In the Add Forward dialog box, enter the following:

    When you click Add, you'll see new this new tag in the Source Editor:

    <forward name="cancel" path="/loginCancel.jsp"/>

    Now, when the user clicks 'Cancel', the isCancelled method is called, and the struts-config.xml 'forwards' (i.e., displays) loginCancel.jsp.

  • Add Logout Functionality. In loginSuccessful.jsp, make sure that the Struts HTML taglib directive is present at the top of the page (just as in loginForm.jsp). Then add this Struts tag below the H1 tags:

    <html:link action="/logout" linkName="Log me out">Logout</html:link>

    But what should happen when this link is clicked? First, notice that the link above references an action called logout. Then, create a JSP page, called loginOut.jsp and change the H1 tags to something like Have a nice day!. Now, when the link is clicked, you want your new page to be opened. So, in struts-config.xml, right-click anywhere, and choose Struts > Add Forward/Include Action. Then specify the action that you've referenced in the link, together with the JSP page that you'd like to display when the link is clicked:

    Click Add. Notice that you've now defined a new action forward:

    <action forward="/loginOut.jsp" path="/logout"/>

    The above tag links the JSP page you created to the link that the user will click in loginSuccessful.jsp to log out of the application.

  • Add Error Handling Functionality. Few things are as disconcerting to a user as seeing a stack trace when a java.lang.RuntimeException (or any other exception) is produced. You want to lessen the blow of the error, so create a nice JSP page that will display your errors and, maybe, even provide some solutions. My error handling page is called loginException.jsp, it contains the taglib directive for the Struts HTML library, and has nothing more than the following between the BODY tags:

    So, Struts is going to output errors to this page. But, which errors? And how will Struts output them here? First, let's force the application to create an error (just for purposes of this example, because in real life you shouldn't need to force your application to create errors). Go back to the isCancelled method in the Action class. Comment out the code that calls loginCancel.jsp, and replace it as follows:

     if (isCancelled(request)){
    throw new java.lang.RuntimeException();
    //return mapping.findForward(CANCEL);
    }

    Here you can see that when the Cancel button is clicked, the java.lang.RuntimeException will be thrown, generating an ugly stack trace for the unsuspecting user. Go back to struts-config.xml, right-click anywhere, and choose Struts > Add Exception. Add the following values in the Add Exception dialog box:

    Click Add. Notice that struts-config.xml now includes the following tag:

    <exception key="message.java.lang.RuntimeException" 
    path="/loginExceptions.jsp"
    type="java.lang.RuntimeException"/>

    Now add the following to the ApplicationResource.properties file:

    message.java.lang.RuntimeException=There was a <b><i>java.lang.runtime.exception</i></b>!

    That's it, you're done. When the user clicks the Cancel button, the java.lang.RuntimeException is thrown, the struts-config.xml
    displays loginException.jsp, and gets the text of the error message from the ApplicationResource.properties file.

So, there you have it—syntax-level validation, business-level validation, cancel functionality, logout functionality, and error-handling functionality. It's all quite basic and not very sophisticated, but you can now kind of see how the Struts world works and how NetBeans IDE 5.0 fits within it. Many thanks to the great Karel Zikmund (currently working for Microsoft in Seattle) who—shortly before leaving the NetBeans QE team—created the application and instructions on which this blog entry is based. Karle, we miss you!

Join the discussion

Comments ( 9 )
  • Graham Reeds Monday, November 21, 2005
    Why do Java developers persist in using Tables in their examples? Tables are for displaying tabulated data - not laying out forms. CSS does that and is a lot tidier too - not a dozen extraneous tags littering the place, just the necessary divs.
    You may say that it is a tutorial but I say why not teach them a little about web design too?
    Since all JSP (and ASP, CGI, etc) will eventually come across to the browser as HTML we might as well follow HTML specs and have the correct DOCTYPE, use valid markup and only use tables when we want to display tabulated data.
    Thanks, Graham
  • Geertjan Monday, November 21, 2005
    That's a fair comment, Graham. Bear in mind that this is a draft, written in the hope that I'll get feedback exactly like yours, so that the final NetBeans Struts tutorial (to be hosted on the NetBeans site, www.netbeans.org, will not have flaws such as the one you point out! Thanks again.
  • vasa Friday, January 6, 2006
    as
  • Angelo Sunday, April 30, 2006
    Hi,
    thank you for this tutorial.
    When you say:
    "Actually, I'd prefer the ActionForm Bean ... please let me know"
    I think that in order to avoid the spurious initial validation you should have a distinct action mapping which is not associated to any form bean, e.g.:
    <action path="/asklogin" forward="/WEB-INF/login.jsp"/>
    Regards,
    Angelo
  • Ellias Donald Motshepa Wednesday, November 22, 2006
    I have this code in my struts-config.xml
    > <data-sources>
    > <data-source
    > type="org.apache.commons.dbcp.BasicDataSource"
    > key="loginDB">
    > <set-property property="driverClassName"
    > value="oracle.jdbc.driver.OracleDriver"/>
    > <set-property property="url"
    > value="jdbc:oracle:thin@itXX:1521:XXXXX"/>
    > <set-property property="username"
    > value="jack"/>
    > <set-property property="password"
    > value="jim"/>
    > </data-source>
    > </data-sources>
    I'm getting this errors when running the application:can u help me
    solve this error pls.
    > org.apache.commons.dbcp.SQLNestedException: Cannot create JDBC driver
    > of class 'oracle.jdbc.driver.OracleDriver' for connect URL
    > 'jdbc:oracle:thin@itXX:1521:XXXXX'
    > at
    > org.apache.commons.dbcp.BasicDataSource.createDataSource(BasicDataSource.java:780)
    > at
    > org.apache.commons.dbcp.BasicDataSource.setLogWriter(BasicDataSource.java:598)
    > at
    > org.apache.struts.action.ActionServlet.initModuleDataSources(ActionServlet.java:808)
    > at
    > org.apache.struts.action.ActionServlet.init(ActionServlet.java:335)
    > at javax.servlet.GenericServlet.init(GenericServlet.java:211)
    > at
    > org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1091)
    > at
    > org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:925)
    > at
    > org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:3880)
    > at
    > org.apache.catalina.core.StandardContext.start(StandardContext.java:4141)
    > at
    > org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:759)
    > at
    > org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:739)
    > at
    > org.apache.catalina.core.StandardHost.addChild(StandardHost.java:524)
    > at
    > org.apache.catalina.startup.HostConfig.deployDescriptor(HostConfig.java:603)
    > at
    > org.apache.catalina.startup.HostConfig.deployDescriptors(HostConfig.java:535)
    > at
    > org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:470)
    > at
    > org.apache.catalina.startup.HostConfig.start(HostConfig.java:1118)
    > at
    > org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:310)
    > at
    > org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:119)
    > at
    > org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1020)
    > at
    > org.apache.catalina.core.StandardHost.start(StandardHost.java:718)
    > at
    > org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1012)
    > at
    > org.apache.catalina.core.StandardEngine.start(StandardEngine.java:442)
    > at
    > org.apache.catalina.core.StandardService.start(StandardService.java:450)
    > at
    > org.apache.catalina.core.StandardServer.start(StandardServer.java:680)
    > at org.apache.catalina.startup.Catalina.start(Catalina.java:536)
    > at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    > at
    > sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    > at
    > sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    > at java.lang.reflect.Method.invoke(Method.java:585)
    > at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:275)
    > at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:413)
    > Caused by: java.sql.SQLException: No suitable driver
    > at java.sql.DriverManager.getDriver(DriverManager.java:243)
    > at
    > org.apache.commons.dbcp.BasicDataSource.createDataSource(BasicDataSource.java:773)
  • asdf Wednesday, October 31, 2007

    asdf


  • NZ Saturday, January 19, 2008

    Couple of notes/comments:

    1. Issues with Cancel Button

    When I tried your code that adds Cancel button, I kept getting 'InvalidCancelException' until I discovered that you have to add 'cacellable' property to your Action defition in struts-config.xml to make it work.

    2. Avoiding error messages at initial load

    Angelo indicated one way, using an 'asklogin' action. I was able to supress the error messages (not the execution of validate() method, mind you), by introducing a hidden form field which acts as a flag. The project's URL (or a direct link to 'login.do') will not set that flag, but a form submission would. Given that my field is called 'realFormSubmission', my validate() method looks something like this:

    public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {

    ActionErrors errors = new ActionErrors();

    if (isRealFormSubmisison()) {

    if (getName() == null || getName().length() < 1) {

    errors.add("nameEmpty", new ActionMessage("error.name.required"));

    }

    if (getPassword() == null || getPassword().length() < 1) {

    errors.add("passwordEmpty", new ActionMessage("error.password.required"));

    }

    }

    return errors;

    }

    Maybe a little crude, but it does the job.


  • Geertjan Sunday, January 20, 2008

    The Cancel button problem is because things have changed in Struts, as explained in the full tutorial for Struts in NetBeans:

    http://www.netbeans.org/kb/60/web/quickstart-webapps-struts.html


  • guest Sunday, September 28, 2008

    hi


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.