A Walking Tour of the Struts MailReader Demonstration Application

This article is meant to introduce a new user to Struts by "walking through" an application. See the Struts Users Guide and Strut's API for more documentation.

The MailReader application is based on the 1.3.0 build of Struts. To follow along, you should install the MailReader application on your own development workstation (e.g. localhost).

This article assumes the reader has a basic understanding of the Java language, JavaBeans, web applications, and JavaServer Pages. For background on these technologies, see the Preface to the Struts User Guide.



The Struts App subproject includes four applications: struts-blank, struts-cookbook, struts-examples, and struts-mailreader. This document walks through struts-mailreader, also known as the "MailReader Demonstration Application".

The premise of the MailReader is that it is the first iteration of a portal application. This version allows users to register themselves and maintain a set of accounts with various mail servers. If completed, the application would let users read mail from their accounts.

The MailReader application demonstrates registering with an application, logging into an application, maintaining a master record, and maintaining child records. This document walks through the constructs needed to do these things, including the server pages, Java classes, and configuration elements.

For more about the MailReader, including alternate implementations and a set of formal Use Cases, please visit the Struts University MailReader site.


JAAS - Note that for compatibility and ease of deployment, the MailReader uses "application-based" authorization. However, use of the standard Java Authentication and Authorization Service (JAAS) is recommended for most applications. (See the Preface to the Struts Action User Guide for more about authentification technologies.)


The walkthrough starts with how the initial welcome page is displayed, and then steps through logging in, adding and editing subscriptions, and creating a new registration.

Howdy

A web application, like any other web site, can specify a list of welcome pages. When you open a web application without specifying a particular page, a welcome page is used by default.

web.xml

When a web application loads, the container reads and parses the "Web Application Deployment Descriptor", or web.xml file. The Struts Action Framework plugs into a web application via a controller servlet. Like any servlet, the Action servlet is deployed via the web.xml.


web.xml - The Web Application Deployment Descriptor
<?xml version="1.0" encoding="ISO-8859-1"?>

    <!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

    <web-app>
    <display-name>Struts MailReader Application</display-name>

    <!-- Action Servlet Configuration -->
    <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
    <param-name>config</param-name>
    <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Servlet Mapping -->
    <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>

    <!-- The Welcome File List -->
    <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- The default error page -->
    <error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/Error.jsp</location>
    </error-page>

    </web-app>

Among other things, the web.xml specifies the "Welcome File List" for an application. When a web address refers to a directory rather than an individual file, the container consults the Welcome File List for the name of a page to open by default.

Albeit, most Struts applications do not refer to physical pages, but to "virtual pages" called actions. The actions are listed in a configuration file. Actions contain code that we want to be run before a page is displayed. An accepted practice in the Struts Action community is to never link directly to server pages, but only to Struts actions. By linking to actions, developers can "rewire" an application without editing the server pages.


Best Practice:

"Link actions not pages."


Unless you are using Java 1.5, actions cannot be specified as a welcome page. So, in the case of a welcome page, how do we follow the Struts best practice of navigating through actions rather than pages?

One solution is to use a server page to "bootstrap" a Struts action. A Java web application recognizes the idea of "forwarding" from one page to another page (or action). We can register the usual "index.jsp" as the welcome page and have it forward to a "Welcome" action.


MailReader's index.jsp
<%@ taglib uri="http://struts.apache.org/tags-logic"
    prefix="logic" %>
    <logic:redirect action="/Welcome"/>

At the top of the page, we import the "struts-logic" JSP tag library. (For more about the Struts JSP tags, see the Struts Taglib website.) The page itself consists of a single tag that redirects to the "Welcome" action. The tag inserts the actual web address for the redirect when the page is rendered. But, where does the tag find the actual address to insert?

Welcome.do

The list of actions, along with other components, are registered through one or more Struts Action configuration files. The configuration files are written as XML documents and processed when the application starts. If we just wanted to forward to the welcome page, we could use a simple configuration element.


A simple "forward thru" action element
        <!-- Display welcome page -->
    <action
    path="/Welcome"
    forward="/Welcome.jsp" />

If someone asked for the Welcome action ("/Welcome.do"), the welcome.jsp page would be displayed in return.

But if we peek at the configuration file for the MailReader, we find a slightly more complicated XML element for the Welcome action.


The Welcome action element
        <!-- Display welcome page -->
    <action
    path="/Welcome"
    
        type="org.apache.struts.apps.mailreader.actions.WelcomeAction"
    >
    <forward
    name="Success"
    path="/Welcome.jsp" />
    </action>

Here, the WelcomeAction Java class executes whenever someone asks for the Welcome action. As it completes, the Action class can select which "result" is displayed. One available result is "Success". Another available result, defined at a global scope, is "Failure". But the Action class doesn't need to know the path to the result, or even if they are server pages. The Action class can select the appropriate result just by using names like "Success" or "Failure".

WelcomeAction

OK ... but why would a WelcomeAction want to choose between Success and Failure?

The MailReader application retains a list of users along with their email accounts. The application stores this information in a database. If the application can't connect to the database, the application can't do its job. So before displaying the welcome page, the class checks to see if the database is available. The MailReader is also an internationalized application. So, the WelcomeAction checks to see if the message resources are available too. If both resources are available, the class forwards to the "Success" path. Otherwise, it forwards to the "Failure" path so that the appropriate error messages can be displayed.

global-forwards


MailReader's global-forward element
    <!-- ==== Global Forward Definitions ==== -->
    <global-forwards>
    <forward
    name="Logoff"
    path="/Logoff.do"/>
    <forward
    name="Logon"
    path="/Logon.do"/>
    <forward
    name="Failure"
    path="/Error.jsp" />
    </global-forwards>

As mentioned, "Failure" is defined in a global scope. Other actions may have trouble connecting to the database later, or other unexpected errors may occur. The MailReader defines the "Failure" result as a Global Forward, so that any action can use it.

MemoryDatabasePlugIn.java

The database is exposed as an object stored in application scope. The database object is based on an interface. Different implementations of the database could be loaded without changing the rest of the application. But how is the database object loaded in the first place?

One section of the Struts configuration is devoted to "PlugIns". When a Struts application loads, it also loads whatever PlugIns are specified in its configuration. The PlugIn interface is quite simple.


org.apache.struts.action.PlugIn
public interface PlugIn {

    void destroy();

    void init(ActionServlet servlet, ModuleConfig config)
    throws ServletException;
    }

You can use PlugIns to do anything that might need to be done when your application loads. The PlugIn is also notified when the application shuts down, so you can release any allocated resources.


The Database PlugIn element
    <plug-in
    className="org.apache.struts.apps.mailreader.plugin.
    MemoryDatabasePlugIn">
    <set-property
    property="pathname"
    value="/WEB-INF/database.xml"/>
    </plug-in>

By default, the MailReader application loads a "MemoryDatabase" implementation of the UserDatabase. MemoryDatabase stores the database content as a XML document, which is parsed and loaded as a set of nested hashtables. The outer table is the list of user objects, each of which has its own inner hashtable of subscriptions. When you register, a user object is stored in this hashtable ... and when you login, the user object is stored within the session context.

The database comes seeded with a sample user. If you check the database.xml file under WEB-INF, you'll see the sample user described in XML.


The "seed" user element from the MailReader database.xml
  <user username="user" fromAddress="John.User@somewhere.com"
    fullName="John Q. User" password="pass">
    <subscription host="mail.hotmail.com" autoConnect="false"
    password="bar" type="pop3" username="user1234">
    </subscription>
    <subscription host="mail.yahoo.com" autoConnect="false" password="foo"
    type="imap" username="jquser">
    </subscription>
    </user>

The "seed" user element creates a registration record for "John Q. User", with the detail for his hotmail account (or "subscription").

MessageResources.properties

Another section of the Struts configuration loads the message resources for the application. If you change a message in the resource, and then reload the application, the change will appear throughout the application. If you provide message resources for additional locales, you can internationalize your application.


The MailReader message-resources element
    <message-resources
    parameter="org.apache.struts.apps.mailreader.resources.ApplicationResources"/>

The "ApplicationResources" parameter refers to a standard properties text file. The resource might be embedded in a JAR or kept under the WEB-INF/classes folder. MailReader uses the classes folder, which means its working copy of the ApplicationResources can be found at ./WEB-INF/classes/org/apache./struts/apps/mailreader/resources/ApplicationResources"

The first time a page needs one of the resources, Struts Action loads and caches the properties file, so that the framework does not need to read the text file a second time.


ApplicationResource entries used by the Welcome page
index.heading=MailReader Demonstration Application
    Options
    index.logon=Log on to the MailReader Demonstration
    Application
    index.registration=Register with the MailReader
    Demonstration Application
    index.title=MailReader Demonstration Application
    index.tour=A Walking Tour of the MailReader Demonstration
    Application

The MailReader application uses a second set of message resources for non-text elements.


The MailReader alternate message-resources element
    <message-resources
    parameter="org.apache.struts.apps.mailreader.resources.AlternateApplicationResources"
    key="alternate"/>

The "key" element can be used to access this resource bundle rather than the default bundle.

Welcome page

After confirming that the necessary resources exist, the WelcomeAction forwards to the Welcome page.


Welcome.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>
    <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>

    <html>
    <head>
    <title><bean:message key="index.title"/></title>
    <link rel="stylesheet" type="text/css" href="base.css" />
    </head>

    <h3><bean:message key="index.heading"/></h3>
    <ul>
    <li><html:link action="/EditRegistration">
    <bean:message key="index.registration"/></html:link></li>
    <li><html:link action="/Logon"><bean:message
    key="index.logon"/></html:link></li>
    </ul>

    <h3>Change Language</h3>
    <ul>
    <li><html:link action="/Locale?language=en">English</html:link></li>
    <li><html:link action="/Locale?language=ja"
    useLocalEncoding="true">Japanese</html:link></li>
    <li><html:link action="/Locale?language=ru"
    useLocalEncoding="true">Russian</html:link></li>
    </ul>

    <hr />

    <p><html:img bundle="alternate"
    pageKey="struts.logo.path" altKey="struts.logo.alt"/></p>

    <p><html:link action="/Tour"><bean:message
    key="index.tour"/></html:link></p>

    </body>
    </html>

At the top of the Welcome page, there are several directives that load the Struts tag libraries. These are just the usual red tape that goes with any JSP file. The rest of the page utilizes three Struts JSP tags: "bean:message", "html:link", and "html:img".

The bean:message tag inserts a message from the MessageResources we mentioned in the last section. The MailReader comes with support for three locales: English (the default), Russian, and Japanese. If the Struts Action locale setting is changed for a user, the bean:message tag will render messages from that locale's property bundle instead.

The html:link tag does double duty. First, you can refer to an action or forward stored in the Struts configuration, and the tag will insert the corresponding path when the page is rendered. This makes it easy to "rewire" an application without touching all the pages. Second, the link tag will "URL encode" the hyperlink to maintain the client session. Your application can maintain client state without requiring cookies.


Tip:

Cookies - If you turn cookies off in your browser, and then reload your browser and this page, you will see the links with the Java session id information attached. (If you are using Internet Explorer and try this, be sure you reset cookies for the appropriate security zone, and that you disallow "per-session" cookies.)


The html:img tag renders an img tag. When necessary, the src URI is encoded as it is with the link tag. In this case, the tag inserts the src path from the "alternate" MessageResource bundle, along with the text for the alt element.

In the span of a single request for the Welcome page, Struts has done quite a bit already:

When rendered, the Welcome page lists two menu options: one to register with the application and one to login in (if you have already registered). Let's follow the Login link first.

Logon Page

If you choose the Logon link, and all goes well, the Logon action forwards control to the Logon.jsp page. The Logon page displays a form that accepts a username and password. You can use the default username and password to logon (user and pass) if you like.

Note that both the username and password are case sensitive. Better yet, try omitting or misspelling the username and password in various combinations to see how the application reacts.


Login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>
    <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>

    <html:xhtml/>
    <html>
    <head>
    <title><bean:message key="logon.title"/></title>
    </head>

    <html:errors/>

    <html:form action="/SubmitLogon" focus="username"
    onsubmit="return validateLogonForm(this);">
    <table border="0" width="100%">

    <tr>
    <th align="right">
    <bean:message key="prompt.username"/>:
    </th>
    <td align="left">
    <html:text property="username" size="16"
    maxlength="18"/>
    </td>
    </tr>

    <tr>
    <th align="right">
    <bean:message key="prompt.password" bundle="alternate"/>:
    </th>
    <td align="left">
    <html:password property="password" size="16"
    maxlength="18"
    redisplay="false"/>
    </td>
    </tr>

    <tr>
    <td align="right">
    <html:submit property="Submit" value="Submit"/>
    </td>
    <td align="left">
    <html:reset/>
    </td>
    </tr>

    </table>

    </html:form>

    <html:javascript formName="LogonForm"
    dynamicJavascript="true"
    staticJavascript="false"/>
    <script language="Javascript1.1" src="StaticJavascript.jsp"></script>

    <jsp:include page="Footer.jsp" />
    </body>
    </html>

We saw some of these tags on the Welcome page. Let's focus on the new tags.

The first new tag on the logon page is html:errors. The credentials you entered are validated and processed by a "LogonAction" class. If the credentials are incorrect, the LogonAction posts an appropriate error message and forwards back to the input page. If the html:errors tag sees that one or more messages were posted, the tag ouputs the messages to the page. The text of the messages can be specified in the MessageResource bundle, making them easy to localize.

The second new tag is html:form. This tag renders a html form tag. The "action" attribute tells the tag to use "SubmitLogon.do" for the form's action. The "focus" attribute tells the tag to generate a little Javascript into the form that sets its focus to the "username" field. The "onsubmit" attribute tells the form to run a Javascript when the form is submitted. (Just like the corresponding attribute on the standard form tag.)

Within the html:form tag, we see five more new tags: "html:text", "html:password", "html:submit", "html:reset", and "html:javascript".

The html:text tag renders a "input type=text" tag. The "property" attribute becomes the input tag's "name" attribute.

The html:password tag renders a "input type=password" tag. The "redisplay" attribute tell the tag not to render the password back into the file, if the submit fails. The html:submit and html:reset tags render buttons of the corresponding types.

Following the form is a html:javascript tag. This tag works with the Struts Validator component to generate a JavaScript that can validate input before it is submitted to the LogonAction.


Tip:

Most of these tags have many more options than the ones we use in this application. For the complete documentation for each tag, see the Tag Developer Guides in the Struts Taglib documentation bundle.


But, how do these tags know so much? How does the Javascript tag know what scripts to write? How do the text and password tags know what values to redisplay?

For the answers, we need to turn back to the Struts Action Framework configuration files, "struts-config.xml" and "validation.xml".

struts-config.xml

The Struts configuration file is parsed by the ActionServlet when the application first load.

web.xml -- Action Servlet Configuration

    <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
    <param-name>config</param-name>
    <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    </servlet>

The SubmitLogon action element
        <!-- Process a user logon -->
    <action
    path="/SubmitLogon"
    type="org.apache.struts.apps.mailreader.actions.LogonAction"
    name="LogonForm"
    scope="request"
    validate="request"
    input="Logon">
    <exception
    key="expired.password"
    type="org.apache.struts.apps.mailreader.dao.ExpiredPasswordException"
    path="/ChangePassword.jsp"/>
    <forward
    name="Success"
    path="/MainMenu.do"/>
    </action>

We saw the path and type attributes in the Welcome action. Let's look at the new attributes: "name", "scope", "input", and "exception".

The "name" attribute

The name attribute specifies something Struts Action calls an "ActionForm". The ActionForm buffers input from a form and delivers it to an Action class as an object. The ActionForm can also be used to validate input. If validation fails, the tags can rewrite the input values from the ActionForm.

The ActionForms are defined in the "form-beans" section of struts-config.

ActionForms can be "conventional" or "dynamic". MailReader uses dynamic ActionForms. Rather than cobble up an actual JavaBean class, we specify the properties the ActionForm can accept in the configuration file.


The form-bean element for LogonForm
        <!-- LogonAction form bean -->
    <form-bean
    name="LogonForm"
    extends="BaseForm"/>

Hmmmm. That doesn't seem to tell us much. The LogonForm "extends" the BaseForm, but doesn't add any attributes. What about the BaseForm element that LogonForm extends?


BaseForm - An "abstract" form-bean
        <!-- BaseAction form bean (abstract) -->
    <form-bean
    name="BaseForm"
    type="org.apache.struts.validator.DynaValidatorForm">
    <form-property
    name="username"
    type="java.lang.String"/>
    <form-property
    name="password"
    type="java.lang.String"/>
    <form-property
    name="task"
    type="java.lang.String"
    initial="Create"/>
    </form-bean>

Note:

Extends - In the Struts configuration file, we can use the "extends" attribute to adopt default settings from another element. Extends makes using XML elements more like object-orientated programming. You can setup a base element, and then only specify the behavior that changes.


Struts creates the ActionForms automatically, regardless of whether the form-bean is conventional or dynamic. The ActionForm lets us specify which properties we want to extract from the incoming request. If a property is not specified by the ActionForm, the value is not automatically transferred from the request, validated, or passed to the Action class.

The "scope" attribute

The scope attribute in the "SubmitLogon" action element tells the controller whether to store the ActionForm in the request, rather than in the user's session.


Best Practice:

Use request scope for single-page forms that contain all the properties needed by the Action. There is usually no need to maintain form input across requests.


The "validate" attribute

The validate attribute controls whether the framework will automatically validate the ActionForm. Conventional ActionForms have a stub "validate" method that you can implement. MailReader uses the Validator framework instead, so the validations are specified in another configuration file.

The "input" attribute

If validation fails, Struts looks for the forward specified by the input attribute. In this case, invoking the global "Logon" forward sends control back to the Logon page.

The "exception" element

Within the SubmitLogon action element is another new element, exception. When a user logons on, it's possible that an "ExpiredPasswordException" will be thrown. Should this happen, Struts will catch the exception and send control to the "ChangePassword" action.


Change Password screen

Your password has expired. Please ask the system administrator to change it. Try Again


OK, it's not the greatest "Change Password" screen -- but, remember, this is still the first iteration!

validations.xml

In the Logon page, we mentioned that the html:javascript tag confers with the Struts Validator components. The Validator is configured through another XML document, the validation.xml.


Validator element for LogonForm
        <form
    name="LogonForm">

    <field
    property="username"
    depends="required">
    <arg
    key="prompt.username"/>
    </field>

    <field
    property="password"
    depends="required, minlength,maxlength">
    <arg
    key="prompt.password"/>
    <arg
    key="${var:minlength}"
    name="minlength"
    resource="false"/>
    <arg
    key="${var:maxlength}"
    name="maxlength"
    resource="false"/>

    <var>
    <var-name>
    maxlength
    </var-name>
    <var-value>
    16
    </var-value>
    </var>
    <var>
    <var-name>
    minlength
    </var-name>
    <var-value>
    3
    </var-value>
    </var>
    </field>

    </form>

The field elements correspond to the ActionForm properties. The username field element says it depends on the "required" validator. If the username is blank or absent, validation will fail and an error message is automatically generated.

The password field (or property) is also required. In addition, it must also pass the "maxlength" and "minlength" validations. Here, the minimum length is three characters and the maximum length is sixteen. If the length of the password doesn't meet these criteria, a corresponding error message is generated. Of course, the messages are generated from the MessageResource bundles and are easy to localize.

LogonAction.java

If validation passes, the LogonForm object is passed to the LogonAction as a parameter. The LogonAction interacts with the database to see if the credentials are valid. If so, the user is logged on, and control passes to the "success" forward. Otherwise, control is forwarded to the input page and the list of error messages displayed.


LogonAction.java
 public final class LogonAction extends BaseAction
    {
    public ActionForward execute(
    ActionMapping mapping,
    ActionForm form,
    HttpServletRequest request,
    HttpServletResponse response)
    throws Exception {

    // Retrieve user
    String username = doGet(form, USERNAME);
    String password = doGet(form, PASSWORD);
    ActionMessages errors = new ActionMessages();
    User user = doGetUser(username,password,errors);

    // Report back any errors, and exit if any
    if (!errors.isEmpty()) {
    this.saveErrors(request, errors);
    return (mapping.getInputForward());
    }

    // Cache user object in session to signify logon
    doCacheUser(request,user);

    // Done
    return doFindSuccess(mapping);
    }
    }

LogonAction extends another Action class, BaseAction. The BaseAction class provides a number of helper methods, all of which are prefixed with "do". The doCacheUser method, for example, stores the user object in the session, and logs the event, if the logging level is set to debug.


BaseAction.doCacheUser
 void doCacheUser(HttpServletRequest request, User
    user) {

    HttpSession session = request.getSession();
    session.setAttribute(Constants.USER_KEY, user);
    if (log.isDebugEnabled()) {
    log.debug(
    "LogonAction: User '"
    + user.getUsername()
    + "' logged on in session "
    + session.getId());
    }
    }

Because our ActionForm is a DynaActionForm, we can't access or change the properties directly using methods like "getUsername". To make working with DynaActionForm properties easier, BaseAction provides two simple helper methods, doGet and doSet. These are utility methods that you caould use in any Struts application.


BaseAction.doGet and doSet
 protected String doGet(ActionForm form, String
    property) {
    String initial;
    try {
    initial = (String) PropertyUtils.getSimpleProperty(form, property);
    } catch (Throwable t) {
    initial = null;
    }
    String value = null;
    if ((initial !=null) && (initial.length()>0)) {
    value = initial.trim();
    if (value.length()==0) value = null;
    }
    return value;
    }

    protected boolean doSet(ActionForm form, String property,
    String value) {
    try {
    DynaActionForm dyna = (DynaActionForm) form;
    dyna.set(property,value);
    } catch (Throwable t) {
    return false;
    }
    return true;
    }

LogonAction uses two other BaseAction helpers: "doGetUser" and "doFindSuccess".


BaseAction.doGetUser
 User doGetUser(UserDatabase database, String
    username,
    String password, ActionMessages errors)
    throws ExpiredPasswordException {

    User user = null;
    if (database == null){
    errors.add(
    ActionMessages.GLOBAL_MESSAGE,
    new ActionMessage("error.database.missing"));
    }
    else {
    user = database.findUser(username);
    if ((user != null) && !user.getPassword().equals(password)) {
    user = null;
    }
    if (user == null) {
    errors.add(
    ActionMessages.GLOBAL_MESSAGE,
    new ActionMessage("error.password.mismatch"));
    }
    }

    return user;
    }

The doGetUser helper does the real work of the LogonAction. The "doGetUser" method checks the tendered credentials against the database, If there are any issues, like there is not a user by that name, or the password doesn't match, doGetUser passes back the appropriate messages.

The doGet user is another "facade" method. A facade simplifies a complicated call to a business method. Here, doGetUser wrapes the business method with the Error Handling needed by the Mail Reader application.


BaseAction.doFindSuccess
 protected ActionForward doFindSuccess
    (ActionMapping mapping) {
    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_SUCCESS);
    }
    return mapping.findForward(Constants.SUCCESS);

The doFindSuccess method is a simple utility but often used. It just checks the mapping for a result named "Success". By using a utility, we can include a logging statement when trace is enabled, and be very sure that everyone knows how to spell "Success". :)

MainMenu.do and MainMenu.jsp

On a successful logon, the Main Menu page displays. If you logged in using the demo account, the page title should be "Main Menu Options for John Q. User". Below this legend should be two links:

If you check the address shown by your browser, you will see that it shows "/SubmitLogon.do" not "/MainMenu.do". The Java servlet platform supports the idea of server-side forwards. When control passed from the SubmitLogon action to the MainMenu action, everything occured server-side. All the browser knows is that we are looking at the result of submitting a form to "/SubmitLogon.do", so that's the address that shows. It doesn't know control passed from one action to another. The difference between server-side forwards and client-side redirects is subtle and often confuses new developers.

If you change the address to "/MainMenu.do" and press enter, the same page will display again, but only because you are logged on. If you bookmark MainMenu.do, log off, and try to return later, the system will force you to logon first.

Let's review the source for the "MainMenu" action mapping, "MainMenuAction.java" class, and the "MainMenu.jsp".


Action mapping element for MainMenu
        <!-- Display MainMenu -->
    <action
    path="/MainMenu"
    type="org.apache.struts.apps.mailreader.actions.MainMenuAction">
    <forward
    name="Success"
    path="/MainMenu.jsp"/>
    </action>
MainMenuAction.java
public final class MainMenuAction extends BaseAction {

    public ActionForward execute (
    ActionMapping mapping,
    ActionForm form,
    HttpServletRequest request,
    HttpServletResponse response)
    throws Exception {

    User user = doGetUser(request);
    if (user==null) return doFindLogon(mapping);
    return doFindSuccess(mapping);
    }

    }
MainMenu.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java"
    %>
    <%@ taglib uri="/tags/app" prefix="app" %>
    <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>
    <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
    <html>
    <head>
    <title><bean:message key="mainMenu.title"/></title>
    <link rel="stylesheet" type="text/css" href="base.css" />
    </head>
    <h3><bean:message key="mainMenu.heading"/> <
    bean:write name="user" property="fullName" /></h3>
    <ul>
    <li><html:link action="/EditRegistration"><bean:message
    key="mainMenu.registration"/></html:link></li>
    <li><html:link action="/Logoff"><bean:message
    key="mainMenu.logoff"/></html:link></li>
    </ul>
    </body>
    </html>

In the MainMenuAction, we find two new helper methods: "doFindLogon" and "doFindSuccess".

The doFindLogon helper locates the "logon" result, and logs the event if trace logging is enabled. If someone has bookmarked "MainMenu.do" and tries to return without logging in, the MainMenuAction sends the request to Logon.do instead.


BaseAction.doFindLogon
 protected ActionForward doFindLogon(ActionMapping
    mapping) {
    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_LOGON);
    }
    return mapping.findForward(Constants.LOGON);
    }

Likewise, doFindSuccess locates the "success" result, and logs the event if trace logging is enabled.


BaseAction.doFindSuccess
 protected ActionForward doFindSuccess
    (ActionMapping mapping) {
    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_SUCCESS);
    }
    return mapping.findForward(Constants.SUCCESS);
    }

The source for MainMenu.jsp also contains a new tag, bean:write. When control passed through the LogonAction, it retrieved a "user" object from the database and stored a reference in "session" scope. This object is a JavaBean with several properties. One property is "fullName". The bean:write tag can find the User bean and print the fullName property for us.

Since the MainMenuAction verifies that the User object exists, the MainMenu page can refer to the object without ado. The application does not reveal the location of any server page on the browser address bar, but if you did happen to know the address for the server page, and you did enter it directly, the tag would throw an exception, and the framework would transfer to the standard error page.


Error.jsp
 An unexpected error has occured

    Cannot find bean: "user" in any scope

    MailReader Demonstration Application

The MainMenu action doesn't have an exception handler of its own, so it falls back to the standard handler in the MailReader web.xml.


error-page element in web.xml
        <!-- The default error page -->
    <error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/Error.jsp</location>
    </error-page>

Meanwhile, The MainMenu page offers two links. One link is to the "Logoff" action. The other is to "EditRegistration". Hmmmm, back on the Welcome page, we also linked to the EditRegistration action to create an account. Which is it? Insert or Update?

EditRegistration.do

If you scan the struts-config for an EditRegisration action, you won't find one. Instead, you will find a "/Edit*" action. The asterisk is a wildcard character that lets you apply a common action configuration to any request that beings with "Edit".


The Wilcard Edit* action and the BaseAction element it extends
        <action
    path="/Edit*"
    extends="//BaseAction"
    parameter="Edit"
    validate="false"/>

    <!-- "Abstract" mapping to use as base -->
    <action path="//BaseAction"
    input="Input"
    type="org.apache.struts.apps.mailreader.actions.{1}
    Action"
    name="{1}Form"
    scope="request">
    <forward
    name="Success"
    path="/"{1}.jsp"/>
    <forward
    name="Input"
    path="/"{1}.jsp"/>
    </action>

The Edit* action extends the "BaseAction" element (as do other Wilcard actions), and provides two attibutes of its own: "validate" and "parameter". Validate we've seen before, but what about "parameter"?

The Action class underlying the EditRegistration action extends a standard Struts class called "MappingDispatchAction". Usually, an Action has one entry point, the "execute" method. Dispatch Actions let you define multiple entry points to an Action. People find Dispatch Actions handy for related actions, like editing or saving the same entity. (In our case, a Registration.)

A MappingDispatchAction uses the parameter attribute to determine which method to use as the entry point. Each dispatch entry point is a separate method, but each method must same signature as execute. In this case, we're telling the framework to call the "Edit" method when this action is invoked.

OK, but how do we know which Action class to call? The "Edit*" action mapping doesn't specify a type attribute.

If you look at the BaseAction mapping, you'll see that it does specify a type attribute, but the class name isn't valid. It contains a "{1}" token instead of the fully qualified class name.

Since the type attribute will be used with a Wildcard action ("/Edit*"), we can use the {1} to represent the first Wildcard replaced in the path. In the case of EditRegistration, the "wild" text would be "Registration", so that's what is plugged into the type statement. Then the EditRegistration action is called, the type becomes "org.apache.struts.apps.mailreader.actions. RegistrationAction".

The same thing will happen for the other markers. The ActionForm name becomes "RegistrationForm". The name of the "Success" server page becomes "Registration.jsp".

The RegistrationForm is a DynaActionForm, much like the LoginForm.


The RegistrationForm form-bean element
    <!-- RegistrationAction form bean -->
    <form-bean
    name="RegistrationForm"
    extends="BaseForm">
    <form-property
    name="fromAddress"
    type="java.lang.String"/>
    <form-property
    name="fullName"
    type="java.lang.String"/>
    <form-property
    name="password2"
    type="java.lang.String"/>
    <form-property
    name="replyToAddress"
    type="java.lang.String"/>
    </form-bean>

The RegistrationForm extends the BaseForm and adds properties peculiar to the Registration action. When either the Welcome page or MainMenu page link to " EditRegistration", the framework creates the RegistrationForm, autopopulates the form-bean properties from any matching attributes in the request, and invokes the Edit method of the RegistrationAction class.


RegistrationAction.Edit
 public ActionForward Edit(
    ActionMapping mapping,
    ActionForm form,
    HttpServletRequest request,
    HttpServletResponse response)
    throws Exception {

    final String method = Constants.EDIT;
    doLogProcess(mapping,method);

    HttpSession session = request.getSession();
    User user = doGetUser(session);
    boolean updating = (user!=null);
    if (updating) {
    doPopulate(form,user);
    }

    doSaveToken(request);
    return doFindSuccess(mapping);
    }

The RegistrationAction.Edit method does most of its work by calling various helper methods, including "doLogProcess", "doGetUser", "doPopulate", and "doSaveToken".


BaseAction.doLogProcess
 protected void doLogProcess(ActionMapping
    mapping, String method) {
    if (log.isDebugEnabled()) {
    StringBuffer sb = new StringBuffer(128);
    sb.append(" ");
    sb.append(mapping.getPath());
    sb.append(":");
    sb.append(Constants.LOG_PROCESSING);
    sb.append(method);
    log.debug(sb.toString());
    }
    }

MailReader uses Commons Logging from Apache Jakarta. Commons Logging is an API utility that can be used with several different logging systems. The doLogProcess helper is just a wrapper around a logging statement that MailReader uses in several places. All of the MailReader's Dispatch methods call this helper to log when control reaches the method.


BaseAction.doGetUser
 protected User doGetUser(HttpSession session) {
    return (User) session.getAttribute(Constants.USER_KEY);
    }

Most of the MailReader actions utilize the User object, and so BaseAction encapsulates obtaining the User into the very simple doGetUser helper.


RegistrationAction.doPopulate
 private void doPopulate(ActionForm form, User
    user) throws ServletException {

    final String title = Constants.EDIT;

    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_POPULATE_FORM + user);
    }

    try {
    PropertyUtils.copyProperties(form, user);
    DynaActionForm dyna = (DynaActionForm) form;
    dyna.set(TASK,title);
    dyna.set(PASSWORD,null);
    dyna.set(PASSWORD2,null);
    } catch (InvocationTargetException e) {
    Throwable t = e.getTargetException();
    if (t == null)
    t = e;
    log.error(LOG_REGISTRATION_POPULATE, t);
    throw new ServletException(LOG_REGISTRATION_POPULATE, t);
    } catch (Throwable t) {
    log.error(LOG_REGISTRATION_POPULATE, t);
    throw new ServletException(LOG_REGISTRATION_POPULATE, t);
    }
    }

The doPopulate helper lives in RegistrationAction rather than BaseAction, since it involves the RegistrationForm, which only the RegistrationAction uses. The main job of doPopulate is to transfer the input values from the ActionForm to the User object. The constants, like "PASSWORD", are defined on RegisrationAction as public constants. These constants document the property names specified in the Struts configuration for the dynamic RegistrationForm.

An interesting point is the line that sets the "TASK" to "Edit". This value is used by the server page to adjust the page title and form layout. When we defined the RegistrationForm, we set the default value for TASK to be "Create". When EditRegistration is called from the Welcome page, we don't have a User object yet, and so the TASK remains at "Create". When EditRegistration is called from the MainMenu page, we do have a User object, and so the TASK is set to "Edit".


BaseAction.doSaveToken
 protected void doSaveToken(HttpServletRequest request) {
    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_TOKEN);
    }
    saveToken(request);
    }

A common problem with designing web applications is that response times can vary and users are impatient. Sometimes, people will press a submit button a second time. When this happens, the browser submits the request again, so that we now have two requests for the same thing. In the case of registering a user, if someone does press the submit button again, and their timing is bad, it could result in the system reporting that the username has already been used. (The first time the button was pressed.) In practice, this would probably never happen, but for a longer running process, like verifying a credit card transaction, it's easier for a double submit to occur.

To forestall double submits, the framework can generate a token that is embedded in the form and also kept in the session. If the value of the tokens do not compare, then we know that there has been a problem, and a form has been submitted twice or out of sequence.

The doSaveToken helper, logs the event for tracking, and then calls the framework method. Later, when we Save the registration, we will check to see if the token is valid.

After the token has been saved, Edit calls "doFindSuccess and" control flows to the Registration page.

Registration page

If you follow the "Edit your user registration profile" link from the Main Menu page, we will finally reach the heart of the MailReader application: the Registration, or "Profile", page. This page displays everything MailReader knows about you (or at least your login), while utilizing several interesting techniques.

To do double duty as the "Create" Registration page and the "Edit" Registration page, the Registation.jsp makes good use of logic tags, to make it appears as though there are two distinct pages. For example, if you are editing the form (task == "Edit"), the page inserts your username from the RegistrationForm bean. If you are new user (task == "Create"), the page creates an empty field, so you can pick your username.


Note:

Presention Logic - The Struts logic tags are a convenient way to express presentation logic within your pages. Customized pages help to prevent user error, and dynamic customization reduces the number of JSPs your application needs to maintain, among other benefits.


The page also uses logic tags to display a list of subscriptions for the given user. If the RegistrationForm has task set to "Edit", the lower part of the page that lists the subscriptions is exposed.


<logic:equal name="RegistrationForm" 
    property="task"
    scope="request" value="Edit">
    <h3><bean:message key="heading.subscriptions"/></h3>
    <!-- ... -->
    <html:link action="/EditSubscription">
    <bean:message key="registration.addSubscription"/>
    </html:link>
    </logic:equal>

Otherwise, the page contains just the top portion -- a blank data-entry form for creating the user's registration.

logic:iterate

Beside making the usual conditional tests, you can also use logic tags to forward control to other actions, to redirect control to another path, and to iterate over collections. The Registration page includes a good example of using the logic:iterate tag to display the user's subscriptions.

The subscriptions are stored in a hashtable object, which is in turn stored in the user object. So to display each subscription, we have to reach into the user object, and loop through the members of the subscription collection. Using the iterate tag, you can code it the way it sounds.


Using logic:iterate to list the Subscriptions
    <logic:iterate name="user" 
    property="subscriptions" id="subscription">
    <tr>
    <td align="left">
    <bean:write name="subscription" property="host"/>
    </td>
    <td align="left">
    <bean:write name="subscription" property="username"/>
    </td>
    <td align="center">
    <bean:write name="subscription" property="type"/>
    </td>
    <td align="center">
    <bean:write name="subscription" property="autoConnect"/>
    </td>
    <td align="center">
    <html:link action="/DeleteSubscription"
    paramName="subscription" paramProperty
    ="host" paramId="host">
    <bean:message key="registration.deleteSubscription"/>
    </html:link>
     
    <html:link action="/EditSubscription"
    paramName="subscription" paramProperty="host" paramId="host">
    <bean:message key="registration.editSubscription"/>
    </html:link>
    </td>
    </tr>
    </logic:iterate>

The three parameters to the iterate tag (name, property, and id) tell it to

  1. name - Check this context for an attribute (e.g. object) named user,
  2. property - Snag the property of user named subscriptions,
  3. id - In the block to iterate, use subscription (singular) as the name for each member of the collection.

Next to each entry in the subscription list are links to Delete and Edit commands. These links use the same name/property/id trinity as the interator, except that the attributes are used to create a hyperlink with a single parameter. (Multiple parameters are possible too, but if the code is well-factored, one should be sufficient.)

Given a subscription to "mail.yahoo.com", the command links would translate to HTML links like these:


The Delete and Edit links for mail.yahoo.com
      <a
    href="/struts-mailreader/DeleteSubscription.do?host=mail.yahoo.com">Delete</a>
     
    <a
    href="/struts-mailreader/EditSubscription.do?host=mail.yahoo.com">Edit</a>

At the foot of the Register page is a link for adding a subscription. Let's wind up the tour by following the Add link and then logging off. Like the link for creating a Registration, Add points to an "Edit" action, namely "EditSubscription".

SubscriptionAction.java

The EditSubscription link shares the Wildcard "/Edit*" mapping we saw with EditRegistration. As before, in the case of "EditSubscription", the "{1}Form" attribute maps to SubscriptionForm.


The SubscriptionAction form-bean element
        <form-bean
    name="SubscriptionForm"
    extends="BaseForm">
    <form-property
    name="autoConnect"
    type="java.lang.Boolean"
        initial="FALSE"
        reset="true"/>
    <form-property
    name="host"
    type="java.lang.String" />
    <form-property
    name="type"
    type="java.lang.String" />
    </form-bean>

The other DynaActionForms we've seen used only String properties. SubscriptionForm is different in that it uses a Boolean type for the "autoConnect" property. On the HTML form, the autoConnect field is represented by a checkbox, and checkboxes need to be handled differently that other controls.


Tip:

Checkboxes - The HTML checkbox is a tricky control. The problem is that, according to the W3C specification, a value is only guaranteed to be sent if the control is checked. If the control is not checked, then the control may be omitted from the request, as if it was on on the page. This can cause a problem with session-scope checkboxes. Once you set the checkbox to true, the control can't set it to false again, because if you uncheck the box, nothing is sent, and so the control stays checked.

The simple solution is to set the initial value for a checkbox control to false before the form is populated. If the checkbox is checked, it will return a value, and the checkbox will represent true. If the checkbox is unchecked, it will not return a value, and the checkbox will remain unchecked ("false").


To be sure the autoConnect checkbox is handled correctly, the SubscriptionForm initializes the property to FALSE, and enables "reset" so that before autopopulation the property is set back to FALSE.

The SubscriptionAction Edit method should look familiar, but it also has a few twists of its own.


SubscriptionAction.Edit
public ActionForward Edit(
    ActionMapping mapping,
    ActionForm form,
    HttpServletRequest request,
    HttpServletResponse response)
    throws Exception {

    final String method = Constants.EDIT;
    doLogProcess(mapping,method);

    HttpSession session = request.getSession();
    User user = doGetUser(session);
    if (user==null) return doFindLogon(mapping);

    // Retrieve the subscription, if there is one
    Subscription subscription;
    String host = doGet(form,HOST);
        boolean updating = (host!=null);
    if (updating) {
    subscription = doFindSubscription(user,host);
    if (subscription==null) return doFindFailure(mapping);
    session.setAttribute(Constants.SUBSCRIPTION_KEY, subscription);
    doPopulate(form,subscription);
    doSet(form,TASK,method);
    }

    return doFindSuccess(mapping);
    }

In RegistrationAction.Edit, we looked for the user object to decide if we were updating or inserting. In SubscriptionAction.Edit, the user object is required (and we trot off to the Login page if it is missing). This could happen because a session expired, or because someone bookmarked a page.

To decide if we are inserting or updating a subscription, we look to see if the host is set to the ActionForm. If it is an update, we fetch the Subscription from the database.


SubscriptionAction.doFindSubscription
 private Subscription doFindSubscription(User
    user, String host) {

    Subscription subscription;

    try {
    subscription = user.findSubscription(host);
    }
    catch (NullPointerException e) {
    subscription = null;
    }

    if ((subscription == null) && (log.isTraceEnabled())) {
    log.trace(
    " No subscription for user "
    + user.getUsername()
    + " and host "
    + host);
    }

    return subscription;
    }

If we can't find the subscription, we use doFindFailure to forward to the Failure result (Error.jsp).


BaseAction.doFindFailure
 protected ActionForward doFindFailure
    (ActionMapping mapping) {
    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_FAILURE);
    }
    return (mapping.findForward(Constants.FAILURE));
    }

In the normal course, the subscription should always be found, since we selected the host from a system-generated list. If the subscription is not found, it would be because the database disappeared or the request is being spoofed.

Like the RegisterAction, the doPopulate method transfers data from the form to the domain object. In this case, a Subscription object.


SubscriptionAction.doPopulate
 private void doPopulate(ActionForm form,
    Subscription subscription) throws ServletException {

    final String title = Constants.EDIT;

    if (log.isTraceEnabled()) {
    log.trace(Constants.LOG_POPULATE_FORM + subscription.getHost());
    }

    try {
    PropertyUtils.copyProperties(form, subscription);
    doSet(form,TASK,title);
    } catch (InvocationTargetException e) {
    Throwable t = e.getTargetException();
    if (t == null) t = e;
    log.error(LOG_SUBSCRIPTION_POPULATE, t);
    throw new ServletException(LOG_SUBSCRIPTION_POPULATE, t);
    } catch (Throwable t) {
    log.error(LOG_SUBSCRIPTION_POPULATE, t);
    throw new ServletException(LOG_SUBSCRIPTION_POPULATE, t);
    }
    }

Most of the code in "doPopulate" is window dressing for the call to PropertyUtils.copyProperties, which does the heavy lifting.

But before turning to our final JSP, a word about our database model ...

User.java and Subscription.java

If you're used to working with relational databases, the links between the user and subscription objects may be confusing. A conventional relational database would create two distinct tables, one for the users and another for the subscriptions, and link them together with a user ID. The MailReader application implements a different model, a hierarchical database. Here a "table" of subscriptions is stored within each user object, something like the way a filing system stores documents within folders.

Development teams often use frameworks like Cayenne to map a relational database to a hierarchy of objects, like the one used by MailReader. For simplicity, the MailReader doesn't use a conventional database, but saves its data as an XML file. While the MailReader is running, the database is kept in main memory, and written to back to disk when changed.

In addition to the usual getters and setters, the user object also has two methods for working with subscription objects. The findSubscription method takes a hostname and returns the subscription object for that host. The getSubscriptions method returns an array of all the subscriptions for the user (ready-made for the iterate tag!). Besides the fields needed to manage the SubscriptionForm data, the object also maintains a runtime link to its own user object.

To create a new subscription, SubscriptionAction.java simply creates a new subscription object, and sets its user to the object found in the request, and then forwards control to its input form, Subscription.jsp.

Subscription.jsp

Saving the best for last, Subscription.jsp utilizes two interesting Struts custom form tags, "html:options" and "html:checkbox".

In Registration.jsp, the Struts iteration tag was used to write a list of subscriptions. Another place where iterations and collections are handy is the option list for a HTML select tag. Since this is such a common situation, Struts offers a html:options (plural) tag that can take an array of objects as a parameter. The tag then iterates over the members of the array (beans) to place each one inside an standard option tag. So given a block like

<html:select property="type">
    <html:options
    collection="serverTypes"
    property="value"
    labelProperty="label" />
    </html:select>

The tag outputs a block like

<select name="type">
    <option value="imap" selected>IMAP Protocol</option>
    <option value="pop3">POP3 Protocol</option>
    </select>

Here, one collection contained both the labels and the values, from properties of the same name. Options can also use a second array for the labels, if they do not match the values. Options can use a Collection, Iterator, or Map for the source of the list.

Unlike other data, the serverTypes array is not fetched from the database. Instead, it is loaded by a Struts plugin. The DigestingPlugin parses an XML document using a given set of Digester rules. The MailReader uses a set of rules for "LabelValueBeans" to create a list of server types.


Tip:

LabelValueBeans - Many developers find the LabelValueBeans useful, so the class is available in the Struts Action distribution as [org.apache.struts.util.LabelValueBean].


The plugin stores the list is stored in application scope. Since the Struts custom tags, like standard JSP tags, search the scopes in succession, the tag finds the list in application scope and uses it to write out the options.

SubscriptionForm.java

Back in Subscription.jsp, we have one more block to cover. Although the same basic form can be used to created, edit, or delete a subscription, people might expect the buttons to be labeled differently in each case. Like the Registration page, the Subscription page handles customization by using a logic tag to output a different set of buttons for each case. Changing buttons doesn't really change the way the Subscription page works, but customizing the buttons does make things less confusing for the user.

<logic:equal
    name="SubscriptionForm"
    property="task"
    scope="request"
    value="Create">
    <html:submit>
    <bean:message key="button.save"/>
</html:submit> </logic:equal>

In the case of a request to delete a subscription, the submit button is labeled "Confirm", since this view is meant to give the user a last chance to cancel, before sending that task along to SaveSubscriptionAction.java.

The actual task property is placed into the form as a hidden field, and SaveSubscriptionAction uses that property to execute the appropriate task.

SubscriptionAction.java

Our final stop has the job of finishing what SubscriptionAction.Edit started. After the usual logic and error checking, The SubscriptionAction.Save method either deletes or updates the subscription object being handled by this request, and cleans up the bean, just to be tidy. By now, you should be very comfortable reading through the source on your own, to pickup the finer points.


SubscriptionAction.Save
 public ActionForward Save(
    ActionMapping mapping,
    ActionForm form,
    HttpServletRequest request,
    HttpServletResponse response)
    throws Exception {

    final String method = Constants.SAVE;
    doLogProcess(mapping,method);

    User user = doGetUser(request);
    if (user == null) {
    return doFindLogon(mapping);
    }

    HttpSession session = request.getSession();
    if (isCancelled(request)) {
    doCancel(session,method,Constants.SUBSCRIPTION_KEY);
    return doFindSuccess(mapping);
    }

    String action = doGet(form,TASK);
    Subscription subscription = doGetSubscription(request);
    boolean isDelete = action.equals(Constants.DELETE);
    if (isDelete) {
    return doRemoveSubscription
    (mapping,session,user,subscription);
    }

    if (subscription==null) {
    subscription = user.createSubscription(doGet(form,HOST));
    session.setAttribute(Constants.SUBSCRIPTION_KEY,subscription);
    }

    doPopulate(subscription,form);
    doSaveUser(user);
    session.removeAttribute(Constants.SUBSCRIPTION_KEY);

    return doFindSuccess(mapping);
    }

This concludes our tour. To review, you may wish to trace the path a new user takes when they register with the application for the first time. You should also read over each of the .java and JSP files carefully, since we only covered the high points here.

Review