You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@struts.apache.org by hu...@apache.org on 2006/04/01 22:38:04 UTC
svn commit: r390747 - in /struts/sandbox/trunk/action2: ./
apps/mailreader/src/java/mailreader2/ apps/mailreader/src/webapp/pages/
Author: husted
Date: Sat Apr 1 12:38:02 2006
New Revision: 390747
URL: http://svn.apache.org/viewcvs?rev=390747&view=rev
Log:
Action2 Apps
* Mailreader Tour
** Complete first full draft of text.
Modified:
struts/sandbox/trunk/action2/README.txt
struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/MailreaderSupport.java
struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/Subscription.java
struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/SubscriptionSave.java
struts/sandbox/trunk/action2/apps/mailreader/src/webapp/pages/tour.html
Modified: struts/sandbox/trunk/action2/README.txt
URL: http://svn.apache.org/viewcvs/struts/sandbox/trunk/action2/README.txt?rev=390747&r1=390746&r2=390747&view=diff
==============================================================================
--- struts/sandbox/trunk/action2/README.txt (original)
+++ struts/sandbox/trunk/action2/README.txt Sat Apr 1 12:38:02 2006
@@ -232,6 +232,8 @@
Issue
+* If the error page is fired by a global exception handler, is the exception going to be logged. Do we need to use a chain result to an action that would log the exception? What would be the Java syntax?
+
* It would be nice to omit the message markup if there is not message.
** http://forums.opensymphony.com/thread.jspa?threadID=7480&messageID=16618#16618
Modified: struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/MailreaderSupport.java
URL: http://svn.apache.org/viewcvs/struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/MailreaderSupport.java?rev=390747&r1=390746&r2=390747&view=diff
==============================================================================
--- struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/MailreaderSupport.java (original)
+++ struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/MailreaderSupport.java Sat Apr 1 12:38:02 2006
@@ -143,6 +143,33 @@
task = value;
}
+ // ---- Token property (utilized by UI) ----
+
+ /**
+ * <p>Field to store double-submit guard.</p>
+ */
+ private String token = null;
+
+
+ /**
+ * <p>Provide Token.</p>
+ *
+ * @return Returns the token.
+ */
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * <p>Store new Token.</p>
+ *
+ * @param value The token to set.
+ */
+ public void setToken(String value) {
+ token = value;
+ }
+
+
// ---- Host property ----
/**
@@ -433,19 +460,14 @@
* <p> Obtain User Subscription object for the given host, or return null
* if not found. </p>
*
+ * <p>It would be possible for this code to throw a NullPointerException,
+ * but the ExceptionHandler in the xwork.xml will catch that for us.</p>
+ *
* @return The matching Subscription or null
*/
public Subscription findSubscription(String host) {
-
Subscription subscription;
-
- try {
- subscription = getUser().findSubscription(host);
- }
- catch (NullPointerException e) {
- subscription = null;
- }
-
+ subscription = getUser().findSubscription(host);
return subscription;
}
@@ -458,7 +480,6 @@
* @return Subscription or null if not found
*/
public Subscription findSubscription() {
-
return findSubscription(getHost());
}
Modified: struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/Subscription.java
URL: http://svn.apache.org/viewcvs/struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/Subscription.java?rev=390747&r1=390746&r2=390747&view=diff
==============================================================================
--- struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/Subscription.java (original)
+++ struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/Subscription.java Sat Apr 1 12:38:02 2006
@@ -38,6 +38,7 @@
types = m;
setHost(getSubscriptionHost());
+
}
/**
Modified: struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/SubscriptionSave.java
URL: http://svn.apache.org/viewcvs/struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/SubscriptionSave.java?rev=390747&r1=390746&r2=390747&view=diff
==============================================================================
--- struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/SubscriptionSave.java (original)
+++ struts/sandbox/trunk/action2/apps/mailreader/src/java/mailreader2/SubscriptionSave.java Sat Apr 1 12:38:02 2006
@@ -5,8 +5,15 @@
*/
public final class SubscriptionSave extends Subscription {
+ public void prepare() {
+ super.prepare();
+ // checkbox workaround
+ }
+
public String execute() throws Exception {
return save();
}
+
+
}
Modified: struts/sandbox/trunk/action2/apps/mailreader/src/webapp/pages/tour.html
URL: http://svn.apache.org/viewcvs/struts/sandbox/trunk/action2/apps/mailreader/src/webapp/pages/tour.html?rev=390747&r1=390746&r2=390747&view=diff
==============================================================================
--- struts/sandbox/trunk/action2/apps/mailreader/src/webapp/pages/tour.html (original)
+++ struts/sandbox/trunk/action2/apps/mailreader/src/webapp/pages/tour.html Sat Apr 1 12:38:02 2006
@@ -177,8 +177,7 @@
<p>
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.
+ then steps through logging into the application and editing a subscription.
</p>
<h3><a name="Welcome" id="Welcome">Welcome Page</a></h3>
@@ -890,7 +889,7 @@
The button's attribute <em>action="Logon!cancel"</em> tells the framework to submit
to the Logon's "cancel" method instead of the usual "execute" method.
The <em>onclick="form.onsubmit=null"</em> script defeats client-side validation.
- On the server side, "cancel" is on a special lists of aliases that bypass validation,
+ On the server side, "cancel" is on a special list of methods that bypass validation,
so the request will go directly to the Action's cancel method.
(Other special aliases on the bypass list include "input" and "back".)
</p>
@@ -1822,7 +1821,7 @@
our tags can access any property of the Action as if it were an implicit property of the page.
The tags don't access the Action directly.
If a textfield tag is told to render the "username" property,
- the tag asks the value stack for the value of "username",
+ the tag asks the value stack for the value of "username",
and the value stack returns the first property it finds by that name.
</p>
@@ -1836,522 +1835,586 @@
<p>
The Subscription list uses another new tag: the <strong>param</strong> tag.
+ As tags go, param takes very few parameters of it's own: just "name" and "value", and neither is required.
+ Although, simple param is one of the most powerful tags the framework provides.
+ Not so much because of what it does, but because of what it allows the other tags to do.
</p>
-<!-- TODO ... -->
+<p>
+ Essentially, the "param" tag provides parameters to other tags.
+ A tag like "text" might be retrieving a message template with several replaceable parameters.
+ No matter how many parameters are in the template, and no matter what they are named,
+ you can use the "param: tag to pass in whatever you need.
+</p>
+
+<pre><code>pager.legend = Displaying {current} of {count} items matching {criteria}.
+...
+<saf:text name="pager.legend">
+ <saf:param name="current" value="42" />
+ <saf:param name="count" value="314" />
+ <saf:param name="criteria" value="Life, the Universe, and Everything" />
+</saf:text></code></pre>
<p>
- 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.)
+ In the case of an "url" tag,
+ we can use "param" to create the query string.
+ A statement like this:
</p>
+<pre><code>
+ <saf:url action="Subscription!edit"><saf:param name="host" value="host"/></saf:url>">
+</code></pre>
+
<p>
- Given a subscription to "mail.yahoo.com",
- the command links would translate to HTML links like these:
+ can render a hyperlink like this:
</p>
-<hr/>
-<h5>The Delete and Edit links for mail.yahoo.com</h5>
-<pre><code> <a
- href="/struts-mailreader/DeleteSubscription.do?host=mail.yahoo.com">Delete</a>
-
- <a
- href="/struts-mailreader/EditSubscription.do?host=mail.yahoo.com">Edit</a></code>
-</pre>
-<hr/>
+<pre><code>
+ <a href="/action2-mailreader/Subscription!edit.do?host=mail.yahoo.com">Edit</a>
+</code></pre>
+
+<!--
<p>
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".
</p>
+-->
-
-
-<h4>
- <a name="SubscriptionAction.java" id="SubscriptionAction.java">SubscriptionAction.java</a>
-</h4>
+<h3>
+ <a name="Subscription" id="Subscription">Subscription</a>
+</h3>
<p>
- The EditSubscription link shares the Wildcard "/Edit*" mapping we saw with
- EditRegistration.
- As before, in the case of "Edit<em>Subscription</em>",
- the "{1}Form" attribute maps to <strong>SubscriptionForm</strong>.
+ If we follow one of the "Edit" subscription links on the Registration page,
+ we come to the "Subscriptions" page,
+ which displays the details of our description in a data-entry form.
+ Let's have a look a the Subscription configuration in xwork.xml
+ and follow the bouncing ball from page to action to page.
</p>
-<hr/>
-<h5>The SubscriptionAction form-bean element</h5>
-<pre><code> <form-bean
- name="SubscriptionForm"
- extends="BaseForm">
- <form-property
- name="autoConnect"
- <strong>type="java.lang.Boolean"
- initial="FALSE"
- reset="true"</strong>/>
- <form-property
- name="host"
- type="java.lang.String" />
- <form-property
- name="type"
- type="java.lang.String" />
- </form-bean></code></pre>
-<hr/>
+<hr />
+<h5>xwork.xml Subscription element</h5>
+<pre><code><action name="Subscription" class="mailreader2.Subscription">
+ <result name="input">/pages/Subscription.jsp</result>
+ <result type="redirect-action">Registration!input</result>
+</action></code></pre>
+<hr />
<p>
- 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.
+ The Edit link specified the Subscription action,
+ but also includes the qualifier <strong>!edit</strong>.
+ The <strong>!</strong> idiom tells the framework to invoke the
+ "edit" method of the Subscription action,
+ instead of the default "execute" method
+ The "alternate" execute methods are called <strong>alias</strong> methods.
</p>
-<hr/>
-<h5>Tip:</h5>
-<blockquote>
- <p class="hint">
- <strong>Checkboxes</strong> -
- 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.
- </p>
-
- <p class="hint">
- 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").
- </p>
-</blockquote>
-<hr/>
+<hr />
+<h5>Subscription edit alias</h5>
+<pre><code>public String <strong>edit()</strong> {
+ <strong>setTask(Constants.EDIT);</strong>>
+ return find();
+}
+
+public String find() {
+ org.apache.struts.apps.mailreader.dao.Subscription
+ sub = findSubscription();
+ if (sub == null) {
+ return ERROR;
+ }
+ <strong>setSubscription(sub);</strong>
+ return INPUT;
+}</code></pre>
+<hr />
<p>
- 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 "edit" alias has two responsiblities.
+ First, it must set the Task property to "Edit".
+ The Subscription page will render itself differently
+ depending on the value of the Task property.
+ Second, edit must locate the relevent Subscription
+ and set it to the Subscription property.
+ If all goes well, edit returns the "input" token,
+ so that the "input" result will be invoked.
</p>
<p>
- The SubscriptionAction Edit method should look familiar, but it also has a
- few twists of its own.
+ 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.
+ If the subscription is not found,
+ edit returns the token for the global "error" result,
+ because this condition is unexpected.
</p>
-<hr/>
-<h5>SubscriptionAction.Edit</h5>
-<pre><code>public ActionForward Edit(
- ActionMapping mapping,
- ActionForm form,
- HttpServletRequest request,
- HttpServletResponse response)
- throws Exception {
-
- final String method = Constants.EDIT;
- doLogProcess(mapping,method);
+<p>
+ The business logic for the edit alias is a simple wrapper
+ around the MailReader DAO classes.
+</p>
- HttpSession session = request.getSession();
- User user = doGetUser(session);
- <strong>if (user==null) return doFindLogon(mapping);</strong>
+<hr />
+<h5>MailreaderSupport findSubscription()</h5>
+<pre><code>public Subscription <strong>findSubscription()</strong> {
+ return findSubscription(getHost());
+}
- // Retrieve the subscription, if there is one
+public Subscription findSubscription(String host) {
Subscription subscription;
- <strong>String host = doGet(form,HOST);
- boolean updating = (host!=null);</strong>
- if (updating) {
- subscription = <strong>doFindSubscription</strong>(user,host);
- if (subscription==null) return <strong>doFindFailure</strong>(mapping);
- session.setAttribute(Constants.SUBSCRIPTION_KEY, subscription);
- <strong>doPopulate</strong>(form,subscription);
- doSet(form,TASK,method);
- }
-
- return doFindSuccess(mapping);
- }</code></pre>
-<hr/>
+ subscription = <strong>getUser().findSubscription(host);</strong>
+ return subscription;
+}</code></pre>
+<hr />
<p>
- 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.
+ This code is very simple
+ and doesn't seem to provide much in the way of error handling.
+ But, that's OK.
+ Since the page is suppose to be entered from a link that we created,
+ we do expect everything to go right here.
+ But, if it doesn't, the global exception handler we defined in xwork.xml
+ will trap the exception for us.
</p>
<p>
- To decide if we are inserting or updating a subscription,
- we look to see if the <strong>host</strong> is set to the ActionForm.
- If it is an update, we fetch the Subscription from the database.
+ Likewise, the AuthentificationInterceptor will ensure that only clients
+ with a valid User object can try to edit a Subscription.
+ If the session expired, or someone bookmarked the page,
+ the client will be redirected to the Logon page.
</p>
-<hr/>
-<h5>SubscriptionAction.doFindSubscription</h5>
-<pre><code> private Subscription <strong>doFindSubscription</strong>(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;
- }</code></pre>
-<hr/>
-
<p>
- If we can't find the subscription,
- we use <strong>doFindFailure</strong> to forward to the Failure result
- (Error.jsp).
+ As a final layer of defines, we also configured a validation for Subscription,
+ to ensure that we are passed a Host parameter.
</p>
-<hr/>
-<h5>BaseAction.doFindFailure</h5>
-<pre><code> protected ActionForward <strong>doFindFailure</strong>
- (ActionMapping mapping) {
- if (log.isTraceEnabled()) {
- log.trace(Constants.LOG_FAILURE);
- }
- return (mapping.findForward(Constants.FAILURE));
- }
-</code></pre>
-<hr/>
+<hr />
+<h5>Subscription-validation.xml</h5>
+<pre><code><!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
+<validators>
+ <field name="<strong>host</strong>">
+ <field-validator type="<strong>requiredstring</strong>">
+ <message key="error.host.required"/>
+ </field-validator>
+ </field>
+</validators></code></pre>
+<hr />
<p>
- 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.
+ By keeping these types of routine safety precautions out of the Action class,
+ the all-important Actions becomes smaller and easier to maintain.
</p>
<p>
- Like the RegisterAction, the <strong>doPopulate</strong> method transfers
- data from the form to the domain
- object. In this case, a Subscription object.
+ After setting the relevent Subscription object to the Subscription property,
+ the framework transfers control to the (you guessed it) Subscription page.
</p>
-<hr/>
-<h5>SubscriptionAction.doPopulate</h5>
-<pre><code> private void <strong>doPopulate</strong>(ActionForm form,
- Subscription subscription) throws ServletException {
-
- final String title = Constants.EDIT;
-
- if (log.isTraceEnabled()) {
- log.trace(Constants.LOG_POPULATE_FORM + subscription.getHost());
- }
+<hr />
+<h5>Subscription.jsp</h5>
+<pre><code><%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/webwork" prefix="saf" %>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <saf:if test="task=='Create'">
+ <title><saf:text name="subscription.title.create"/></title>
+ </saf:if>
+ <saf:if test="task=='Edit'">
+ <title><saf:text name="subscription.title.edit"/></title>
+ </saf:if>
+ <saf:if test="task=='Delete'">
+ <title><saf:text name="subscription.title.delete"/></title>
+ </saf:if>
+ <link href="<saf:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+ </head>
+ <body onLoad="self.focus();document.Subscription.username.focus()">
+
+ <saf:actionerror/>
+ <saf:form method="POST" <strong>action="SubscriptionSave"</strong> validate="false">
+ <strong><saf:token /></strong>
+ <strong><saf:hidden name="task"/></strong>
+ <strong><saf:label label="%{getText('username')}" name="user.username"/></strong>
+
+ <saf:if test="task == 'Create'">
+ <saf:textfield label="%{getText('mailHostname')}" name="host"/>
+ </saf:if>
+ <saf:else>
+ <saf:label label="%{getText('mailHostname')}" name="host"/>
+ <saf:hidden name="host"/>
+ </saf:else>
+
+ <saf:if test="task == 'Delete'">
+ <saf:label label="%{getText('mailUsername')}"
+ name="subscription.username"/>
+ <saf:label label="%{getText('mailPassword')}"
+ name="subscription.password"/>
+ <saf:label label="%{getText('mailServerType')}"
+ name="subscription.type"/>
+ <saf:label label="%{getText('autoConnect')}"
+ name="subscription.autoConnect"/>
+ <saf:submit value="%{getText('button.confirm')}"/>
+ </saf:if>
+ <saf:else>
+ <saf:textfield label="%{getText('mailUsername')}"
+ name="subscription.username"/>
+ <saf:textfield label="%{getText('mailPassword')}"
+ name="subscription.password"/>
+ <strong><saf:select label="%{getText('mailServerType')}"
+ name="subscription.type" list="types"/></strong>
+ <strong><saf:checkbox label="%{getText('autoConnect')}"
+ name="subscription.autoConnect"/></strong>
+ <saf:submit value="%{getText('button.save')}"/>
+ <saf:reset value="%{getText('button.reset')}"/>
+ </saf:else>
+
+ <saf:submit action="Registration!input"
+ value="%{getText('button.cancel')}"
+ onclick="form.onsubmit=null"/>
+ </saf:form>
+
+ <jsp:include page="Footer.jsp"/>
+ </body>
+</html></code></pre>
+
+<p>
+ As before, we'll discuss the tags and attributes that are new to this page:
+ "token", "hidden", "label", "select", and "checkbox".
+</p>
+
+<p>
+ When we looked at the form tag for the Logon page,
+ it did not specify a target for the submit.
+ Instead, it just posted back to the Logon action.
+ In this <strong>form</strong> tag, we are specifying a different action,
+ <strong>SubscriptionSave</strong>
+ to be the target of the submit,
+</p>
+
+<p>
+ The main reason we use another action is so that we can use a different set of validations.
+ When we retrieve the Subscription for editing, all we need is the Host property.
+ When we save the Subscription, we want to validate additional properties.
+ Since the validation files are coupled to the classes,
+ we created a new Action class for saving a Subscription.
+</p>
- try {
- <strong>PropertyUtils.copyProperties(form, subscription);</strong>
- 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);
- }
- }</code></pre>
-<hr/>
+<hr />
+<h5>Subscription-validation.xml</h5>
+<pre><code><!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
+ "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
+<validators>
+ <field name="<strong>host</strong>">
+ <field-validator type="<strong>requiredstring</strong>">
+ <message key="error.host.required"/>
+ </field-validator>
+ </field>
+</validators></code></pre>
+<hr />
<p>
- Most of the code in "doPopulate" is window dressing for the call to
- <strong>PropertyUtils.copyProperties</strong>, which does the heavy
- lifting.
+ The validators follow the same type of inheritance path as the classes.
+ SubscriptionSave.java extends Subscription.java,
+ so when SubscriptionSave is validated,
+ the Host property specified by "Subscription-validation.xml" will also be required.
</p>
<p>
- But before turning to our final JSP, a word about our database model ...
+ The <strong>token</strong> tag works with the Token Session Interceptor to foil double submits.
+ The tag generates a key that is embedded in the form and cached in the session.
+ Without this tag, the Interceptor can't work it's magic.
</p>
-<h4>
- <a name="User.java" id="User.java">User.java</a> and <a
- name="Subscription.java" id="Subscription.java">Subscription.java</a>
-</h4>
-
<p>
- 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.
+ The <strong>hidden</strong> tag embeds the Task property into the form.
+ When the form is submitted,
+ the SubscriptionSave action wil use the Task property to decide
+ whether to insert or update the form.
</p>
<p>
- Development teams often use frameworks like <a
- href="http://www.objectstyle.org/cayenne/">Cayenne</a>
- 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.
+ The <strong>label</strong> renders a "read only" version of a property,
+ suitable for placement in the form.
+ In Edit or Delete mode, we want the Host property to be immutable,
+ since it is used as a key. (As unwise as that might sound.)
+ In Delete mode, all of the properties are immutable,
+ since we are simply confirming the delete operation.
</p>
<p>
- In addition to the usual getters and setters,
- the user object also has two methods for working with subscription
- objects.
- The <strong>findSubscription</strong> method takes a hostname and returns
- the subscription object for that host.
- The <strong>getSubscriptions</strong> 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.
+ Saving the best for last, the Subscription utilizes two more interesting
+ tags, "select" and "checkbox".
</p>
+
<p>
- 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.
+ Unsuprisingly, the <strong>select</strong> tag renders a select control,
+ but the tag goes so without requiring a lot of markup or redtape.
</p>
-<h3><a name="subcription.jsp" id="subcription.jsp">Subscription.jsp</a></h3>
+<pre><code><saf:select label="%{getText('mailServerType')}"
+ name="subscription.type" <strong>list="types"</strong> />
+</code></pre>
<p>
- Saving the best for last, Subscription.jsp utilizes two interesting Struts
- custom form tags,
- "html:options" and "html:checkbox".
+ The interesting attribute of the "select" tag is "list",
+ which, in our case, specifies a value of "types".
+ If we take another look at the Subscription action,
+ we can see that it implements an interface named "Preparable"
+ and populates a "types" property in a method named "prepare".
+</p>
+
+<hr />
+<h5>Subscription-validation.xml</h5>
+<pre><code>public class <strong>Subscription</strong> extends MailreaderSupport
+ <strong>implements Preparable</strong> {
+
+ private Map types = null;
+ public Map <strong>getTypes()</strong> {
+ return types;
+ }
+
+ public void <strong>prepare()</strong> {
+ Map m = new LinkedHashMap();
+ m.put("imap", "IMAP Protocol");
+ m.put("pop3", "POP3 Protocol");
+ types = m;
+ setHost(getSubscriptionHost());
+ }
+
+ // ... </code></pre>
+<hr />
<p>
- 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</p>
+ The default Interceptor stack includes the PrepareInterceptor,
+ which observes the Preparable interface.
+</p>
-<pre><code><html:select property="type">
- <html:options
- collection="serverTypes"
- property="value"
- labelProperty="label" />
- </html:select></code></pre>
+<hr />
+<h5>PrepareInterceptor</h5>
+<pre><code>public class <strong>PrepareInterceptor</strong> extends AroundInterceptor {
-<p>The tag outputs a block like</p>
+ protected void after(ActionInvocation dispatcher, String result) throws Exception {
+ }
-<pre><code><select name="type">
- <option value="imap" selected>IMAP Protocol</option>
- <option value="pop3">POP3 Protocol</option>
- </select></code></pre>
+ protected void before(ActionInvocation invocation) throws Exception {
+ Object action = invocation.getAction();
+ <strong>if (action instanceof Preparable) {
+ ((Preparable) action).prepare();</strong>
+ }
+ }
+}</code></pre>
<p>
- 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.
+ The PrepareInterceptor ensures that the "prepare" method will always be called
+ before "execute" or an alias method is invoked.
+ We use prepare to setup the list of items for the select list to display.
+ We also transfer the Host property from our Subscription object
+ to a local property, where it is easier to manage.
</p>
<p>
- Unlike other data, the serverTypes array is not fetched from the database.
- Instead, it is loaded by a Struts plugin.
- The <strong>DigestingPlugin</strong> 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.
+ The <strong>checkbox</strong> starts out as a simple enough control.
</p>
-<hr/>
-<h5>Tip:</h5>
-<blockquote>
- <p><font class="hint">
- <strong>LabelValueBeans</strong> -
- Many developers find the LabelValueBeans useful,
- so the class is available in the Struts Action distribution as
- [org.apache.struts.util.LabelValueBean].
- </font></p>
-</blockquote>
-<hr/>
+<pre><code><saf:checkbox label="%{getText('autoConnect')}"
+ name="subscription.autoConnect"/></code></pre>
<p>
- 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.
+ The Subscription object has a boolean AutoConnect property,
+ and the checkbox simply has to represent its state.
+ The problem is, if you clear a checkbox, the browser client will not submit <em>anything</em>.
+ Nada. Zip.
+ It is as if the checkbox control never existed.
+ The HTTP protocol has no way to affirm "false".
+ if the control is missing, we need to figure out it's been unclicked.
</p>
-
-<h4><a name="SubscriptionForm.java" id="SubscriptionForm.java">SubscriptionForm.java</a>
-</h4>
-
<p>
- 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.
+ If you are backing the form with an object in session state, as MailReader does,
+ the behavior is problematic because, without some extra work,
+ the client cannot uncheck the box.
+ If the session-state property is set to true, and the client unchecks the box,
+ the control is not submitted.
+ There is no trigger to change state, and so state remains true.
</p>
-<pre><code><logic:equal
- name="SubscriptionForm"
- <strong>property="task"</strong>
- scope="request"
- value="Create">
- <html:submit>
- <b><bean:message key="button.save"/><br/></b> </html:submit>
- </logic:equal></code></pre>
-
<p>
- 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 simplest solution is to employ our old friend Prepare again.
+ In the "prepare" method for SubscriptionSave,
+ we can set the property represented by the checkbox to false.
+ If the control is not submitted, then the property remains false.
+ If the control is submitted, then the property is set to true.
</p>
-<p>
- The actual task property is placed into the form as a hidden field,
- and SaveSubscriptionAction uses that property to execute the appropriate
- task.
-</p>
+<hr />
+<h5>SubscriptionSave</h5>
+<pre><code>public final class SubscriptionSave extends Subscription {
+
+ public void prepare() {
+ super.prepare();
+ // checkbox workaround
+ <strong>getSubscription().setAutoConnect(false);</strong>
+ }
+
+ public String execute() throws Exception {
+ return save();
+ }
+}</code></pre>
-<h4><a name="SubscriptionAction.java" id="SubscriptionAction.java">SubscriptionAction.java</a>
+<h4>
+ <a name="SubscriptionAction.java" id="SubscriptionAction.java">SubscriptionAction.java</a>
</h4>
<p>
- 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.
+ 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.
</p>
<hr/>
-<h5>SubscriptionAction.Save</h5>
-<pre><code> public ActionForward <strong>Save</strong>(
- 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)) {
- <strong>doCancel</strong>(session,method,Constants.SUBSCRIPTION_KEY);
- return doFindSuccess(mapping);
- }
-
- String action = doGet(form,TASK);
- Subscription subscription = <strong>doGetSubscription</strong>(request);
- boolean isDelete = action.equals(Constants.DELETE);
- if (isDelete) {
- return <strong>doRemoveSubscription</strong>
- (mapping,session,user,subscription);
- }
-
- if (subscription==null) {
- subscription = <strong>user.createSubscription</strong>(doGet(form,HOST));
- session.setAttribute(Constants.SUBSCRIPTION_KEY,subscription);
- }
-
- doPopulate(subscription,form);
- <strong>doSaveUser</strong>(user);
- session.removeAttribute(Constants.SUBSCRIPTION_KEY);
+<h5>Tip:</h5>
+<blockquote>
+ <p class="hint">
+ <strong>Checkboxes</strong> -
+ 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.
+ </p>
- return doFindSuccess(mapping);
- }</code></pre>
+ <p class="hint">
+ 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").
+ </p>
+</blockquote>
<hr/>
<p>
- 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.
+ If we press the SAVE button,
+ the form will be submitted to the SubscriptionSave action.
+ If the validation succeeds, as we've seen,
+ SubscriptionSave will invoke the Subscription.Save method.
</p>
-<h3><a name="Review" id="Review">Review</a></h3>
+<hr />
+<h5>Subscription save</h5>
+<pre><code>public String save() throws Exception {
-<ul>
- <li>Struts uses a single controller servlet to route HTTP requests.</li>
+ if (Constants.DELETE.equals(getTask())) {
+ removeSubscription();
+ }
+
+ if (Constants.CREATE.equals(getTask())) {
+ copySubscription(getHost());
+ }
+
+ saveUser();
+ return SUCCESS;
+}</code></pre>
+<hr />
- <li>The requests are routed to action objects according to path (or
- URI).</li>
+<p>
+ The <strong>save</strong> method uses the Task property to handle
+ the special cases of deleting and creating,
+ and then updates the state of the User object.
+</p>
- <li>Each request is handled as a separate thread</li>
+<p>
+ The <strong>removeSubscription</strong> method calls the DAO facade,
+ and then updates the application state.
+</p>
+
+<hr />
+<h5>removeSubscription</h5>
+<pre><code>public void removeSubscription() throws Exception {
+ getUser().removeSubscription(getSubscription());
+ getSession().remove(Constants.SUBSCRIPTION_KEY);
+}</code></pre>
+<hr />
- <li>There is only one object for each action (URI), so your action objects
- must be multi-thread safe.</li>
+<p>
+ The <strong>copySubscription</strong> method is a bit more intertesting.
+ The MailReader DAO layer API includes some immutable fields
+ that can't be set once the object is created.
+ Because key fields are immutable,
+ We can't just create a Subscription, let the framework populate all the fields,
+ and jsut save it when we are done, because some fields can't be populated,
+ except at construction.
+</p>
- <li>The configuration of action objects are loaded from a XML resource
- file, rather than hardcoded.</li>
+<p>
+ Once workaround would be to declare properties on the Action
+ for all the properties we need to pass to the Subscription or User objects.
+ When we are ready to create the object, we can pass it values from the Action.
+</p>
- <li>Action objects can respond to the request, or ask the controller to
- forward the request to another object or to another page, such as an
- input form.</li>
+<p>
+ Another workaround is to declare only the immutable properties on the Action,
+ and then use what we can from the domain object.
+</p>
- <li>A library of custom tags works with the rest of the framework to
- enhance use of JavaServer Pages.</li>
+<p>
+ This implementation of the MailReader utilizes the second alternative.
+ We define User and Subscription objects on our base Action,
+ and add other properties only as needed.
+</p>
- <li>The Struts form tag can work closely with an action objects via a
- Struts ActionFormBean to retain the state of a data-entry form, and
- validate the data entered.</li>
+<p>
+ To add a new Subscription or User,
+ we create a blank object to capture whatever fields we can.
+ When this "input" object returns, we create a new object,
+ setting the immutable fields to appropriate values,
+ and copy over the rest of the properties.
+</p>
- <li>ActionForm beans can be automatically created by the JSP form or
- controller servlet.</li>
+<hr />
+<h5>copySubscription</h5>
+<pre><code>public void copySubscription(String host) {
+ Subscription input = getSubscription();
+ Subscription sub = createSubscription(host);
+ if (null != sub) {
+ BeanUtils.setValues(sub, input, null);
+ setSubscription(sub);
+ setHost(sub.getHost());
+ }
+}</code></pre>
+
+<p>
+ Of course, this is not a preferred solution,
+ but merely a way to work around an issue in the MailReader DAO API
+ that would not be easy for us change.
+</p>
+
+<h3>Summary</h3>
+<p>
+ At this point, we've booted the application, logged on,
+ reviewed a Registeration record, and edited a Subscription.
+ Of course, there's more, but from here on, it is mostly more of the same.
+ The full source code for MailReader is
+ <a href="http://svn.apache.org/viewcvs.cgi/struts/sandbox/trunk/action2/apps/mailreader/">available online</a>
+ and in the distribution.
+</p>
- <li>Struts supports a message resource for loading constants strings.
- Alternate message resources can be provided to internationalize an
- application.</li>
-</ul>
-<hr/>
-</blockquote>
</body>
</html>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@struts.apache.org
For additional commands, e-mail: dev-help@struts.apache.org