http://git-wip-us.apache.org/repos/asf/struts/blob/fa9cac70/apps/mailreader/src/main/webapp/tour.html ---------------------------------------------------------------------- diff --git a/apps/mailreader/src/main/webapp/tour.html b/apps/mailreader/src/main/webapp/tour.html deleted file mode 100644 index 7dc29f0..0000000 --- a/apps/mailreader/src/main/webapp/tour.html +++ /dev/null @@ -1,2470 +0,0 @@ -<!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"> -<head> - <meta http-equiv="Content-Type" content="text/html; charset=us-ascii"/> - <link rel="stylesheet" type="text/css" href="../css/mailreader.css"/> - - <title>A Walking Tour of the Struts 2 MailReader Application</title> -</head> - -<body> -<blockquote> -<h2>A Walking Tour of the Struts 2 MailReader Application</h2> - -<p> - <i> - This article is meant to introduce a new user to Apache Struts 2 by - "walking through" a simple, but functional, application. - The article includes code snippets, but for the best result, you might - want to install the MailReader application on your own development - workstation and follow along. - Of course, the full source code to the MailReader is included in the - distribution. - </i> -</p> - -<p> - <i> - The tour assumes the reader has a basic understanding of the Java - language, JavaBeans, web applications, and JavaServer Pages. For - background on these technologies, see the - <a href="http://struts.apache.org/primer.html"> - Key Technologies Primer</a>. - </i> -</p> - -<hr/> - -<ul> - <li> - <a href="#Welcome">Welcome</a> - - <ul> - <li><a href="#web.xml">web.xml and resources.properties</a></li> - - <li><a href="#Welcome.do">Welcome.do</a></li> - - <li><a href="#Welcome.java">Welcome Action</a></li> - - <li><a href="#global-results">Global Results</a></li> - - <li><a href="#ApplicationListener.java">ApplicationListener.java</a></li> - - <li><a href="#resources.properties">Message Resources</a></li> - - <li><a href="#Welcome.jsp">Welcome Page</a></li> - - </ul> - </li> -</ul> - -<ul> - <li> - <a href="#Login">Login</a> - <ul> - - <li><a href="#Login.jsp">Login Page</a></li> - - <li><a href="#Login-validation.xml">Login-validation.xml</a></li> - - <li><a href="#Login.java">Login.java</a></li> - - <li><a href="#MailreaderSupport.java">MailreaderSupport.java</a></li> - - <li><a href="#Login.xml">Login Configuration</a></li> - - </ul> - </li> -</ul> - -<ul> - <li> - <a href="#MainMenu">MainMenu</a> - </li> -</ul> - -<ul> - <li> - <a href="#Registration.jsp">Registration page</a> - <ul> - <li><a href="#iterator">iterator</a></li> - </ul> - </li> -</ul> - -<ul> - <li> - <a href="#Subscription">Subscription</a> - - <ul> - <li><a href="#Subscription.java">Subscription.java</a> - </li> - </ul> - </li> -</ul> -<hr/> - -<p> - The premise of the MailReader is that it is the first iteration of a - portal application. - This version allows users to register and maintain a set of - accounts with various mail servers. - If completed, the application would let users read mail from their - accounts. -</p> - -<p> - The MailReader application demonstrates registering with an application, - logging into an application, maintaining a master record, and maintaining - child records. - This article overviews the constructs needed to do these things, - including the server pages, Java classes, and configuration elements. -</p> - -<p> - For more about the MailReader, including alternate implementations and a - set of formal Use Cases, - please visit the <a href="http://www.StrutsUniversity.org/MailReader"> - Struts University MailReader site</a>. -</p> - -<hr/> -<blockquote> - <p><font class="hint"> - <strong>JAAS</strong> - - 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 <a - href="http://struts.apache.org/primer.html"> - Key Technologies Primer</a> for more about - authentication technologies.) - </font></p> -</blockquote> -<hr/> - -<p> - The tour starts with how the initial welcome page is displayed, and - then steps through logging into the application and editing a subscription. - Please note that this not a quick peek at a "Hello World" application. - The tour is a rich trek into a realistic, best practices application. - You may need to adjust your chair and get a fresh cup of coffee. - Printed, the article is 29 pages long (US). -</p> - -<h3><a name="Welcome" id="Welcome">Welcome Page</a></h3> - -<p> - 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 - default "welcome page" is served as the response. -</p> - -<h4><a name="web.xml" id="web.xml">web.xml</a></h4> - -<p> - When a web application loads, - the container reads and parses the "Web Application Deployment - Descriptor", or "web.xml" file. - The framework plugs into a web application via a servlet filter. - Like any filter, the "struts2" filter is deployed via the "web.xml". -</p> - -<hr/> -<h5>web.xml - The Web Application Deployment Descriptor</h5> -<pre><code><?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 2 MailReader</display-name> - - <strong><filter> - <filter-name>struts2</filter-name> - <filter-class> - org.apache.struts2.dispatcher.FilterDispatcher - </filter-class> - </filter></strong> - - <filter-mapping> - <filter-name><strong>struts2</strong></filter-name> - <url-pattern>/*</url-pattern> - </filter-mapping> - - <listener> - <listener-class> - org.springframework.web.context.ContextLoaderListener - </listener-class> - </listener> - - <!-- Application Listener for MailReader database --> - <listener> - <listener-class> - mailreader2.ApplicationListener - </listener-class> - </listener> - - <welcome-file-list> - <welcome-file>index.html</welcome-file> - </welcome-file-list> - - </web-app></code></pre> -<hr/> - -<p> - You might note that the web.xml configuration does not specify which file extension - to use with actions. - The default extension for Struts 2 is ".action", - but the extension can be changed in the struts.properties file. - For compatability with prior releases, the MailReader uses a .do extension for actions. -</p> - -<hr/> -<h5>struts.properties</h5> -<pre><code>struts.action.extension = <strong>do</strong></code></pre> -<hr/> - -<p> - The web.xml does specify a "Welcome File List" for the 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. -</p> - -<p> - However, most Struts applications do not refer to physical pages, - but to "virtual resources" called <i>actions</i>. - Actions specify code that we want to be run before a page - or other resource renders the response. - An accepted practice is to never link directly to server pages, - but only to logical action mappings. - By linking to actions, developers can often "rewire" an application - without editing the server pages. -</p> - -<hr/> -<h5>Best Practice:</h5> -<blockquote> - <p><font class="hint">"Link actions not pages."</font></p> -</blockquote> -<hr/> - -<p> - The actions are listed in one or more XML configuration files, - the default configuration file being named "struts.xml". - When the application loads, the struts.xml, and any other files - it includes, are parsed, and the framework creates a set of - configuration objects. - Among other things, the configuration maps a request for a certain - page to a certain action mapping. -</p> - - -<p> - Sites can list zero or more "Welcome" pages in the web.xml. - <a href="http://forum.java.sun.com/thread.jspa?threadID=721445"> - Unless you are using Java 1.5,</a> - actions cannot be specified as a Welcome page. - So, in the case of a Welcome page, - how do we follow the best practice of navigating through actions - rather than pages? -</p> - -<p> - One solution is to use a page to "bootstrap" one of our actions. - We can register the usual "index.html" as the Welcome page and have it - redirect to a "Welcome" action. -</p> - -<hr/> -<h5>MailReader's index.html</h5> -<pre><code><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html><head> - <META HTTP-EQUIV="Refresh" CONTENT="0;<strong>URL=Welcome.do</strong>"> - </head> - <body> - <p>Loading ...</p> -</body></html></code></pre> -<hr/> - -<p> - As an alternative, - we could also have used a JSP page that issued the redirect with a Struts tag, - but a plain HTML solution works as well. -</p> - -<h4><a name="Welcome.do" id="Welcome.do">Welcome.do</a></h4> - -<p> - When the client requests "Welcome.do", the request is passed to the "struts2" - FilterDispatcher (that we registered in the web.xml file). - The FilterDispatcher retrieves the appropriate action mapping from the - configuration. - If we just wanted to forward to the Welcome page, we could use a simple - configuration element. -</p> -<hr/> -<h5>A simple "forward thru" action element</h5> -<pre><code><action name="<strong>Welcome</strong>"> - <result><strong>/pages/Welcome.jsp</strong></result> -</action></code></pre> -<hr/> - -<p> - If a client asks for the Welcome action ("Welcome.do"), the "/page/Welcome.jsp" - page would be returned in response. - The client does not know, or need to know, that the physical resource is located at - "/pages/Welcome.jsp". - All the client knows is that it requested the resource "Welcome.do". -</p> - -<p> - But if we peek at the configuration file for the MailReader, - we find a slightly more complicated XML element for the Welcome action. -</p> - -<hr/> -<h5>The Welcome action element</h5> -<pre><code><action name="Welcome" <b>class="mailreader2.Welcome"</b>> - <result>/pages/Welcome.jsp</result> - <strong><interceptor-ref name="guest"/></strong> - </action></code></pre> -<hr/> - -<p> - Here, the <strong>Welcome</strong> Java class executes whenever - someone asks for the Welcome action. - As it completes, the Action class can select which "result" is displayed. - The default result name is "success". - Another available result, defined at a global scope, is "error". -</p> - -<hr/> -<h5>Key concept:</h5> -<blockquote> - <p> - The Action class doesn't need to know what result type is needed - for "success" or "error". - The Action can just return the logical name for a result, - without knowing how the result is implemented. - </p> -</blockquote> -<hr/> - -<p> - The net effect is that all of the result details, - including the paths to server pages, - all can be declared <em>once</em> in the configuration. - Tightly coupled implementation details are not scattered all over - the application. -</p> - -<hr/> -<h5>Key concept:</h5> -<blockquote> - <p> - The Struts configuration lets us separate concerns and "say it once". - The configuration helps us "normalize" an application, - in much the same way we normalize a database schema. - </p> -</blockquote> -<hr/> - - -<p> - OK ... but why would a Welcome Action want to choose between "success" and - "error"? -</p> - -<h4><a name="Welcome.java" id="Welcome.java">Welcome Action</a></h4> - -<p> - 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 <strong>page</strong>, the Welcome - <strong>class</strong> checks to see if the database is available. -</p> - -<p> - The MailReader is also an internationalized application. - So, the Welcome Action class checks to see if the message resources are - available too. - If both resources are available, the class passes back the "success" token. - Otherwise, the class passes back the "error" token, - so that the appropriate messages can be displayed. -</p> - -<hr/> -<h5>The Welcome Action class</h5> -<pre><code>package mailreader2; -public class Welcome extends MailreaderSupport { - - public String execute() { - - // Confirm message resources loaded - String message = getText(Constants.ERROR_DATABASE_MISSING); - if (Constants.ERROR_DATABASE_MISSING.equals(message)) { - <strong>addActionError(Constants.ERROR_MESSAGES_NOT_LOADED);</strong> - } - - // Confirm database loaded - if (null==getDatabase()) { - <strong>addActionError(Constants.ERROR_DATABASE_NOT_LOADED);</strong> - } - - if (hasErrors()) { - <strong>return ERROR;</strong> - } - else { - <strong>return SUCCESS;</strong> - } - } -}</code></pre> -<hr/> - -<p> - Several common result names are predefined, - including ERROR, SUCCESS, LOGIN, NONE, and INPUT, - so that these tokens can be used consistently across Struts 2 applications. -</p> - - -<h4><a name="global-results" id="global-results">Global Results</a></h4> - -<p> - As mentioned, "error" 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 "error" result as a Global Result, - so that any action can use it. -</p> - -<hr/> -<h5>MailReader's global-result element</h5> -<pre><code> <global-results> - <result name=<strong>"error"</strong>><strong>/pages/Error.jsp</strong></result> - <result name="invalid.token">/pages/Error.jsp</result> - <result name="login" type="redirect-action">Login_input</result> -</global-results></code></pre> -<hr/> - -<p> - Of course, if an individual action mapping defines its own "error" result type, - the local result would be used instead. -</p> - -<h4><a name="ApplicationListener.java" id="ApplicationListener.java">ApplicationListener.java</a> -</h4> - -<p> - 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? -</p> - -<p> - The database is created by a custom Listener that we configured in the "web.xml". -</p> - -<hr/> -<h5>mailreader2.ApplicationListener</h5> -<pre><code> <listener> - <listener-class> - <strong>mailreader2.ApplicationListener</strong> - </listener-class> -</listener></code></pre> -<hr/> - -<p> - By default, our ApplicationListener loads a <strong>MemoryDatabase</strong> - 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. - When you login, the user object is stored within the session context. -</p> - -<p> - The database comes seeded with a sample user. - If you check the "database.xml" file under "/src/main/resources", - you'll see the sample user described in XML. -</p> - -<hr/> -<h5>The "seed" user element from the MailReader database.xml</h5> -<pre><code><user username="<strong>user</strong>" fromAddress="john.u...@somewhere.com" - fullName="<strong>John Q. User</strong>" password="<strong>pass</strong>"> - <subscription host="<strong>mail.hotmail.com"</strong> autoConnect="false" - password="bar" type="pop3" username="user1234"> - </subscription> - <subscription host="<strong>mail.yahoo.com</strong>" autoConnect="false" password="foo" - type="imap" username="jquser"> - </subscription> -</user></code></pre> -<hr/> - -<p> - The "seed" user element creates a registration record for "John Q. User", - with the subscription detail for his hotmail and yahoo accounts. -</p> - -<h4><a name="resources.properties" id="resources.properties">Message Resources</a> -</h4> - -<p> - As mentioned, MailReader is an internationalized application. - In Struts 2, message resources are associated with the Action class being processed. - If we check the source, we find a language resource bundle named - <em>MailreaderSupport.</em> - MailreaderSupport is our base class for all the MailReader Actions. - Since all of our Actions extend MailreaderSupport, - all of our Actions can use the same resource bundle. -</p> - -<hr/> -<h5>Message Resource entries used by the Welcome page</h5> -<pre><code><strong>index.heading=</strong>MailReader Application Options -<strong>index.login=</strong>Log on to the MailReader Application -<strong>index.registration=</strong>Register with the MailReader Application -<strong>index.title=</strong>MailReader Demonstration Application -<strong>index.tour=</strong>A Walking Tour of the MailReader Demonstration Application</code></pre> -<hr/> - -<p> - If you change a message in the resource, and then rebuild and reload the - application, the change will appear throughout the application. - If you provide message resources for additional locales, you can - localize your application. - The MailReader provides resources for English, Russian, and Japanese. -</p> - -<h4><a name="Welcome.jsp" id="Welcome.jsp">Welcome Page</a></h4> - -<p> - After confirming that the necessary resources exist, the Welcome action - forwards to the Welcome page. -</p> -<hr/> -<h5>Welcome.jsp</h5> -<pre><code><%@ page contentType="text/html; charset=UTF-8" %> -<strong><%@ taglib prefix="s" uri="http://struts.apache.org/tags" %></strong> - <!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> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> - <title><strong><s:text name="index.title"/></strong></title> - <link href="<strong><s:url value="/css/mailreader.css"/></strong>" rel="stylesheet" - type="text/css"/> - </head> - - <body> - <h3><s:text name="index.heading"/></h3> - - <ul> - <li><a href="<s:url action="Registration_input"/>"><s:text - name="index.registration"/></a></li> - <li><a href="<s:url action="Login_input"/>"><s:text - name="index.login"/></a></li> - </ul> - - <h3>Language Options</h3> - <ul> - <li> - <s:url id="en" action="Welcome"> - <s:param name="request_locale">en</s:param> - </s:url> - <s:a href="%{en}">English</s:a> - </li> - <li> - <s:url id="ja" action="Welcome"> - <s:param name="request_locale">ja</s:param> - </s:url> - <s:a href="%{ja}">Japanese</s:a> - </li> - <li> - <s:url id="ru" action="Welcome"> - <s:param name="request_locale">ru</s:param> - </s:url> - <s:a href="%{ru}">Russian</s:a> - </li> - </ul> - - <hr /> - - <p><strong><s:i18n name="alternate"></strong> - <img src="<s:text name="struts.logo.path"/>" - alt="<s:text name="struts.logo.alt"/>"/> - <strong></s:i18n></strong></p> - - <p><a href="<s:url action="Tour" />"><s:text name="index.tour"/></a></p> - - </body> -</html></code></pre> -<hr/> - -<p> - At the top of the Welcome page, there are several directives that load the - Struts 2 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: - "text", "url", and "i18n". -</p> - -<p> - (We use the tag prefix "s:" in the Struts 2 MailReader application, - but you can use whatever prefix you like in your applications.) -</p> - -<p> - The <strong>text</strong> tag inserts a message from an - application's default resource bundle. - If the framework's locale setting is changed for a user, - the text tag will render messages from the new locale's resource - bundle instead. -</p> - -<p> - The <strong>url</strong> tag can render a reference to an - action or any other web resource, - applying "URL encoding" to the hyperlinks as needed. - Java's URL encoding feature lets your application maintain client state - without requiring cookies. -</p> - -<hr/> -<h5>Tip:</h5> -<blockquote> - <p><font class="hint"> - <strong>Cookies</strong> - - 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.) - </font></p> -</blockquote> -<hr/> - -<p> - The <strong>i18n</strong> tag provides access to multiple resource bundles. - The MailReader application uses a second set of message resources for - non-text elements. - When these are needed, we use the "i18n" tag to specify a different bundle. -</p> - -<p> - The <strong>alternate</strong> bundle is stored in the {{/src/main/resources}} folder, - so that it ends up under "classes", which is on the application's class path. -</p> - -<p> - In the span of a single request for the Welcome page, the framework has done - quite a bit already: -</p> - -<ul> - <li> - Confirmed that required resources were loaded during initialization. - </li> - - <li> - Written all the page headings and labels from internationalized - message resources. - </li> - - <li> - Automatically URL-encoded paths as needed. - </li> -</ul> - -<p> - When rendered, the Welcome page lists two menu options: - one to register with the application and one to log on (if you have - already registered). - Let's follow the Login link first. -</p> - -<h3><a name="Login" id="Login">Login</a></h3> - -<p> - If you choose the Login link, and all goes well, the Login action forwards - control to the Login page. -</p> - -<h4><a name="Login.jsp" id="Login.jsp">Login Page</a></h4> - -<p> - The Login page displays a form that accepts a username and password. - You can use the default username and password to login - (<strong>user</strong> and <strong>pass</strong>), if - you like. Try omitting or misspelling the username and password in - various combinations to see how the application reacts. - Note that both the username and password are case sensitive. -</p> - -<hr/> -<h5>Login.jsp</h5> -<pre><code><%@ page contentType="text/html; charset=UTF-8" %> - <%@ taglib prefix="s" uri="http://struts.apache.org/tags" %> - <!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> - <title><s:text name="login.title"/></title> - <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet" - type="text/css"/> - </head> - <body onLoad="self.focus();document.Login.username.focus()"> - <strong><s:actionerror/></strong> - <strong><s:form action="Login" validate="true"></strong> - <strong><s:textfield key="username"/></strong> - <strong><s:password key="password"/></strong> - <strong><s:submit key="button.save"/></strong> - <strong><s:reset key="button.reset"/></strong> - <s:submit <strong>action="Login_cancel" onclick="form.onsubmit=null"</strong> - key="button.cancel"/> - </s:form> - <jsp:include page="Footer.jsp"/> - </body> -</html></code></pre> -<hr/> - -<p> - We already saw some of the tags used by the Login page on the Welcome page. - Let's focus on the new tags. -</p> - -<p> - The first new tag on the Login page is <strong>actionerrors</strong>. - Most of the possible validation errors are related to a single field. - If you don't enter a username, - the framework can place an error message near the tag prompting you to - enter a username. - But some messages are not related to a single field. - For example, the database might be down. - If the action returns an "Action Error", as opposed to a "Field Error", - the messages are rendered in place of the "actionerror" tag. - The text for the validation errors, whether they are Action Errors or - Field Errors, can be specified in the resource bundle, - making the messages easy to manage and localize. -</p> - -<p> - The second new tag is <strong>form</strong>. - This tag renders a HTML form tag. - The "validate=true" setting enables client-side validation, - so that the form can be validated with JavaScript before being sent - back to the server. - The framework will still validate the form again, just to be sure, but the - client-side validation can save a few round-trips to the server. -</p> - -<p> - Within the form tag, - we see four more new tags: "textfield", "password", "submit", - and "reset". We also see a second usage of "submit" that utilizes an - "action" attribute. -</p> - -<p> - When we place a control on a form, we usually need to code a set of - HTML tags to do everything we want to do. - Most often, we do not just want a plain "input type=text" tag. - We want the input field to have a label too, and maybe even - a tooltip. And, of course, a place to print a message - should invalid data be entered. -</p> - -<p> - The Struts Tags support templates and themes so that a set of HTML tags can be - rendered from a single Struts Tag. For example, the single tag -</p> - -<pre><code> - <s:<strong>textfield</strong> key="username"/> -</code></pre> - -<p> - generates a wad of HTML markup. -</p> - -<hr/> -<pre><code><tr> - <td class="tdLabel"> - <label for="Login_username" class="label">Username:</label> - </td> - <td> - <input type="text" name="username" value="" id="Login_username"/> - </td> -</tr></code></pre> -<hr/> - -<p> - If for some reason you don't like the markup generated by a Struts Tag, - it's each to change. - Each tag is driven by a template that can be updated on a tag-by-tag basis. - For example, - here is the default template that generates the markup for the ActionErrors tag: -</p> - -<hr/> -<pre><code><#if (actionErrors?exists && actionErrors?size > 0)> - <ul> - <#list actionErrors as error> - <li><span class="errorMessage">${error}</span></li> - </#list> - </ul> -</#if></code></pre> -<hr/> - -<p> - If you wanted ActionErrors displayed in a table instead of a list, - you could edit a copy of this file, save it as a file named - "template/simple/actionerror.ftl", - and place this one file at the base of your application's classpath. -</p> - -<hr/> -<pre><code><#if (actionErrors?exists && actionErrors?size > 0)> - <strong><table></strong> - <#list actionErrors as error> - <strong><tr><td></strong><span class="errorMessage">${error}</span><strong></td></tr></strong> - </#list> - <strong></table></strong> -</#if></code></pre> -<hr/> - -<p> - Under the covers, the framework uses - <a href="http://freemarker.sourceforge.net/">Freemarker</a> - for its standard templating language. - FreeMarker is similar to - <a href="http://jakarta.apache.org/velocity/">Velocity</a>, - but it offers better error reporting and some additional features. - If you prefer, Velocity and JSP templates can also be used to create your own tags. -</p> - -<p> - The <strong>password</strong> tag renders a "input type=password" - tag, along with the usual template/theme markup. - By default, the password tag will not retain input if the submit fails. - If the username is wrong, - the client will have to enter the password again too. - (If you did want to retain the password when validation fails, - you can set the tag's "showPassword" property to true.) -</p> - -<p> - Unsurprisingly, the <strong>submit</strong> and <strong>reset</strong> tags - render buttons of the corresponding types. -</p> - -<p> - The second submit button is more interesting. -</p> - -<pre><code> <s:submit <strong>action="Login_cancel" onclick="form.onsubmit=null"</strong> - key="button.cancel"/> -</code></pre> - -<p> - Here we are creating the Cancel button for the form. - The button's attribute <em>action="Login<strong>_</strong>cancel"</em> - tells the framework to submit to the Login'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 list of methods that bypass validation, - so the request will go directly to the Action's <strong>cancel</strong> method. - Another entry on the special-case list is the "input" method. -</p> - -<hr/> -<h5>Tip:</h5> -<blockquote> - <p><font class="hint"> - The Struts Tags have options and capabilities beyond what we have shown here. - For more see, the <a href="http://cwiki.apache.org/WW/tag-developers-guide.html"> - Struts Tag documentation.</a> - </font></p> -</blockquote> -<hr/> - -<p> - OK, but how do the tags know that both of these fields are required? - How do they know what message to display when the fields are empty? -</p> - -<p> - For the answers, we need to look at another flavor of configuration file: - the "validation" file. -</p> - -<h4><a name="Login-validation.xml" id="Login-validation.xml">Login-validation.xml</a> -</h4> - -<p> - While it is not hard to code data-entry validation into an Action class, - the framework provides an even easier way to validate input. -</p> - -<p> - The validation framework is configured through another XML document, the <strong> - Login-validation.xml</strong>. -</p> - -<hr/> -<h5>Validation file for Login Action</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>username</strong>"> - <field-validator type="<strong>requiredstring</strong>"> - <message key="<strong>error.username.required</strong>"/> - </field-validator> - </field> - <field name="<strong>password</strong>"> - <field-validator type="<strong>requiredstring</strong>"> - <message key="<strong>error.password.required</strong>"/> - </field-validator> - </field> -</validators> -</code></pre> -<hr/> - -<p> - You may note that the DTD refers to "XWork". - <a href="http://www.opensymphony.com/xwork/"> - Open Symphony XWork - </a> is a generic command-pattern framework that can be used outside of a - web environment. Essentially, Struts 2 is a web-based extension of the - XWork framework. -</p> - -<p> - The field elements correspond to the ActionForm properties. - The <strong>username</strong> and <strong>password</strong> field elements - say that each field depends on the "requiredstring" validator. - If the username is blank or absent, validation will fail and an error - message is generated. - The messages would be based on the "error.username.required" or - "error.password.required" message templates from the resource bundle. -</p> - -<!-- -<p> - The <strong>password</strong> 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. -</p> ---> - -<h4><a name="Login.java" id="Login.java">Login Action</a></h4> - -<p> - If validation passes, the framework invokes the "execute" method of the Login Action. - The actual Login Action is brief, since most of the functionality derives - from the base class, <strong>MailreaderSupport</strong>. -</p> - -<hr/> -<h5>Login.java</h5> -<pre><code>package mailreader2; -import org.apache.struts.apps.mailreader.dao.User; -public final class <strong>Login</strong> extends MailreaderSupport { -public String <strong>execute()</strong> throws ExpiredPasswordException { - User user = <strong>findUser(getUsername(), getPassword());</strong> - if (user != null) { - <strong>setUser(user);</strong> - } - if (<strong>hasErrors()</strong>) { - return INPUT; - } - return SUCCESS; - } -}</code></pre> -<hr/> - -<p> - Login lays out what we do to authenticate a user. - We try to find the user using the credentials provided. - If the user is found, we cache a reference. - If the user is not found, we return "input" so the client can try again. - Otherwise, we return "success", so that the client can access the rest of the application. -</p> - -<h4><a name="MailreaderSupport.java" id="MailreaderSupport.java">MailreaderSupport.java</a></h4> - -<p> - Let's look at the relevant properties and methods from MailreaderSupport - and another base class, <strong>ActionSupport</strong>, namely - "getUsername", "getPassword", "findUser", "setUser", and "hasErrors". -</p> - -<p> - The framework lets you define - <a href="http://struts.apache.org/primer.html#javabeans">JavaBean properties</a> - directly on the Action. - Any JavaBean property can be used, including rich objects. - When a request comes in, - any public properties on the Action class are matched with the request parameters. - When the names match, the request parameter value is set to the JavaBean property. - The framework will make its best effort to convert the data, - and, if necessary, it will report any conversion errors. -</p> - -<p> - The <strong>Username</strong> and <strong>Password</strong> properties are nothing fancy, - just standard JavaBean properties. -</p> - -<hr/> -<h5>MailreaderSupport.getUsername() and getPassword()</h5> -<pre><code>private String username = null; -public String <strong>getUsername()</strong> { - return this.username; -} -public void setUsername(String username) { - this.username = username; -} - -private String password = null; -public String <strong>getPassword()</strong> { - return this.password; -} -public void setPassword(String password) { - this.password = password; -}</code></pre> -<hr/> - -<p> - We use these properties to capture the client's credentials, - and pass them to the more interesting <strong>findUser</strong> method. -</p> - -<hr/> -<h5>MailreaderSupport.findUser</h5> -<pre><code>public User <strong>findUser</strong>(String username, String password) - throws <strong>ExpiredPasswordException</strong> { - User user = <strong>getDatabase().findUser(username)</strong>; - if ((user != null) && !user.getPassword().equals(password)) { - user = null; - } - if (user == null) { - this.<strong>addFieldError</strong>("password", getText("error.password.mismatch")); - } - return user; -}</code></pre> -<hr/> - -<p> - The "findUser" method dips into the MailReader Data Access Object layer, - which is represented by the <strong>Database</strong> property. - The code for the DAO layer is maintained as a separate component. - The MailReader application imports the DAO JAR, - but it is not responsible for maintaining any of the DAO source. - Keeping the data access layer at "arms-length" is a very good habit. - It encourages a style of development where the data access layer - can be tested and developed independently of a specific end-user application. - In fact, there are several renditions of the MailReader application, - all which share the same MailReader DAO JAR! -</p> - -<hr/> -<h5>Best Practice:</h5> -<blockquote> - <p> - <font class="hint">"Strongly separate data access and business logic from the rest of - the application."</font> - </p> -</blockquote> -<hr/> - -<p> - When "findUser" returns, - the Login Action looks to see if a valid (non-null) User object is returned. - A valid User is passed to the <strong>User property</strong>. - Although it is still a JavaBean property, - the User property is not implemented in quite the same way as Username and Password. -</p> - -<hr/> -<h5>MailreaderSupport.setUser</h5> -<pre><code>public User getUser() { - return (User) <strong>getSession().get(Constants.USER_KEY)</strong>; -} -public void setUser(User user) { - getSession().put(Constants.USER_KEY, user); -}</code></pre> -<hr/> - -<p> - Instead of using a field to store the property value, - "setUser" passes it to a <strong>Session</strong> property. -</p> - -<hr /> -<h5>MailreaderSupport.getSession() and setSession()</h5> -<pre><code>private Map session; -public Map <strong>getSession()</strong> { - return session; - -public void <strong>setSession(Map value)</strong> { - session = value; -}</code></pre> -<hr /> - -<p> - To look at the MailreaderSupport class, - you would think the Session property is a plain-old Map. - In fact, - the Session property is an adapter that is backed by the servlet session object at runtime. - The MailreaderSupport class doesn't need to know that though. - It can treat Session like any other Map. - We can also test the MailreaderSupport class by passing it some other implementation of - Map, running the test, - and then looking to see what changes MailreaderSupport made to our "mock" Session object. -</p> - -<p> - But, when MailreaderSupport is running inside a web application, - how does it acquire a reference to the servlet session? -</p> - -<p> - Good question. If you were to look at just the MailreaderSupport class, - you would not see a single line of code that sets the session property. - But, yet, when we run the class, the session property is not null. - Hmmm. -</p> - -<p> - The magic that provides the Session property a runtime value is called - "dependency injection". - The MailreaderSupport class implements a interface called <strong>SessionAware</strong>. - SessionAware is bundled with the framework, - and it defines a setter for the Session property. -</p> - -<p> - <code>public void <strong>setSession</strong>(Map session);</code> -</p> - -<p> - Also bundled with the framework is an object called the - <strong>ServletConfigInterceptor</strong>. - If the ServletConfigInterceptor sees that an Action implements the SessionAware interface, - it automatically set the session property. -</p> - -<pre><code>if (action instanceof <code>SessionAware</code>) { - ((SessionAware) action).<code>setSession</code>(context.getSession()); -}</code></pre> - -<p> - The framework uses these "Interceptor" classes to create a <strong>front controller</strong> - for each action an application defines. - Each Interceptor can peek at the request before an Action class is invoked, - and then again after the Action class is invoked. - (If you have worked with Servlet - <a href="http://struts.apache.org/primer.html#filters">Filters</a>, - you will recognize this pattern. - But, unlike Filters, Interceptors are not tied to HTTP. - Interceptors can be tested and developed outside of a web application.) -</p> - -<p> - You can use the same set of Interceptors for all your actions, - or define a special set of Interceptors for any given action, - or define different sets of Interceptors to use with different types of actions. - The framework comes with a default set of Interceptors, - that it will use when another set is not specified, - but you can designate your own default Interceptor set (or "stack") - in the Struts configuration. -</p> - -<p> - Many Interceptors provide a utility or helper functions, - like setting the session property. - Others, like the <strong>ValidationInterceptor</strong>, - can change the workflow of an action. - Interceptors are key feature of the framework, - and we will see a few more on the tour. -</p> - -<p> - If a valid User is not found, or the password doesn't match, - the "findUser" method invokes the <strong>addFieldError</strong> method to note the - problem. - When "findUser" returns, the Login Action checks for errors, - and then it returns either INPUT or SUCCESS. -</p> - -<p> - The "addFieldError" method is provided by the ActionSupport class, - which is bundled with the framework. - The constants for INPUT and SUCCESS are also provided by ActionSupport. - While the ActionSupport class provides many useful utilities, - you are not required to use it as a base class. - Any Java class can be used as an Action, if you like. -</p> - -<p> - It is a good practice to provide a base class with utilities - that can be shared by an application's Action classes. - The framework does this with ActionSupport, - and the MailReader application does the same with the MailreaderSupport class. -</p> - -<hr/> -<h5>Best Practice:</h5> -<blockquote> - <p><font class="hint">"Use a base class to define common functionality."</font></p> -</blockquote> -<hr/> - -<p> - But, what happens if Login returns INPUT instead of SUCCESS. - How does the framework know what to do next? -</p> - -<p> - To answer that question, - we need to turn back to the Struts configuration - and look at how Login is declared. -</p> - - -<h4><a name="Login.xml" id="Login.xml">Login Configuration</a></h4> - -<p> - The Login action element outlines how the Login workflow operates, - including what to do when the Action returns "input", - or the default result name "success". -</p> - -<hr/> -<h5>mailreader-support.xml Login</h5> -<pre><code><action name="<strong>Login_*</strong>" method="{1}" class="mailreader2.Login"> - <result name="<strong>input</strong>">/pages/Login.jsp</result> - <result name="<strong>cancel</strong>" type="redirect-action">Welcome</result> - <result type="redirect-action">MainMenu</result> - <result name="<strong>expired</strong>" type="chain">ChangePassword</result> - <<strong>exception-mapping</strong> - exception="org.apache.struts.apps.mailreader.dao.ExpiredPasswordException" - result="<strong>expired</strong>"/> - <interceptor-ref name="<strong>guest</strong>"/> -</action></code></pre> -<hr/> - -<p> - You might notice that the name of the Login action element is not "Login" - but "Login<strong>_*</strong>". - The asterisk is a special "wildcard" notation that tells the framework to match any series - of character at this point. - In the method attribute, - the "{1}" notation indicates that framework should substitute whatever characters match - the asterisk at runtime. - When we cite actions like "Login_cancel" or "Login_input", - the framework matches "cancel" or "input" with the wildcard and fills in the blanks. -</p> - -<p> - The "trailing bang" notation was hardwired into WebWork 2. - To provide backward compatibility, - the notation is supported by Struts 2.0. - If you prefer to use wildcards to emulate the same notation, - as the Mailreader does, - you should disable the old notation in the Struts properties file. -</p> - -<hr/> -<h5>struts.properties</h5> -<pre><code>struts.enable.DynamicMethodInvocation = false</code></pre> -<hr/> - -<p> - Using wildcards with a exclamation point (or "bang") is not the only way we can use - wilcards to invoke methods. - If we wanted to use actions like "inputLogin", - we could move the asterisk and use an action name like "*Login". -</p> - -<p> - Within the Login action element, the first result element is named "input". - If validation or authentification fail, - the Action class will return "input" and the framework will transfer control to the - "Login.jsp" page. -</p> - -<p> - The second result element is named <strong>cancel</strong>. - If someone presses the cancel button on the Login page, - the Action class will return "cancel", this result will be selected, - and the framework will issue a redirect to the Welcome action. -</p> - -<p> - The third result has no name, - so it will be called if the default <strong>success</strong> token is returned. - So, if the Login succeeds, - control will transfer to the MainMenu action. -</p> - -<p> - The MailReader DAO exposes a "ExpiredPasswordException". - If the DAO throws this exception when the User logs in, - the framework will process the exception-mapping - and transfer control to the "ChangePassword" action. -</p> - -<p> - Just in case any other Exceptions are thrown, - the MailReader application also defines a global handler. -</p> - -<hr/> -<h5>mailreader-default.xml exception-mapping</h5> -<pre><code><global-exception-mappings> - <exception-mapping - result="error" - exception="java.lang.Exception"/> -</global-exception-mappings></code></pre> -<hr/> - -<p> - If an unexpected Exception is thrown, - the exception-mapping will transfer control to the action's "error" result, - or to a global "error" result. - The MailReader defines a global "error" result - which transfers control to an "Error.jsp" page - that can display the error message. -</p> - -<hr/> -<h5>Error.jsp</h5> -<pre><code><%@ page contentType="text/html; charset=UTF-8" %> -<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %> - <!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> - <title>Unexpected Error</title> - </head> - <body> - <h2>An unexpected error has occured</h2> - <p> - Please report this error to your system administrator - or appropriate technical support personnel. - Thank you for your cooperation. - </p> - <hr /> - <h3>Error Message</h3> - <strong><s:actionerror /></strong> - <p> - <strong><s:property value="%{exception.message}"/></strong> - </p> - <hr /> - <h3>Technical Details</h3> - <p> - <strong><s:property value="%{exceptionStack}"/></strong> - </p> - <jsp:include page="Footer.jsp"/> - </body> -</html></code></pre> -<hr/> - -<p> - The Error page uses <strong>property</strong> tags to expose - the Exception message and the Exception stack. -</p> - -<p> - Finally, the Login action specifies an <strong>InterceptorStack</strong> - named <strong>defaultStack.</strong> - If you've worked with Struts 2 or WebWork 2 before, that might seem strange, - since "defaultStack" is the factory default. -</p> - -<p> - In the MailReader application, most of the actions are only available - to authenticated users. - The exceptions are the Welcome, Login, and Register actions - which are available to everyone. - To authenticate clients, - the MailReader uses a custom Interceptor and a custom Interceptor stack. -</p> - -<hr/> -<h5>mailreader2.AuthenticationInterceptor</h5> -<pre><code>package mailreader2; -import com.opensymphony.xwork2.interceptor.Interceptor; -import com.opensymphony.xwork2.ActionInvocation; -import com.opensymphony.xwork2.Action; -import java.util.Map; -import org.apache.struts.apps.mailreader.dao.User; - -public class <strong>AuthenticationInterceptor</strong> implements Interceptor { - public void destroy () {} - public void init() {} - public String <strong>intercept</strong>(ActionInvocation actionInvocation) throws Exception { - Map session = actionInvocation.getInvocationContext().getSession(); - User user = (User) session.get(Constants.USER_KEY); - boolean isAuthenticated = (null!=user) && (null!=user.getDatabase()); - if (<strong>isAuthenticated</strong>) { - return actionInvocation.invoke(); - } - else { - return Action.LOGIN; - } - } -}</code></pre> -<hr/> - -<p> - The <strong>AuthenticationInterceptor</strong> looks to see if a User object - has been stored in the client's session state. - If so, it returns normally, and the next Interceptor in the set would be invoked. - If the User object is missing, the Interceptors returns "login". - The framework would match "login" to the global result, - and transfer control to the Login action. -</p> - -<p> - The MailReader defines three custom Interceptor stacks: "user", "user-submit", - and "guest". -</p> - -<hr/> -<h5>mailreader-default.xml interceptors</h5> -<pre><code><interceptors> - <interceptor name="<strong>authentication</strong>" - class="mailreader2.AuthenticationInterceptor"/> - <interceptor-stack name="<strong>user</strong>" > - <interceptor-ref name="authentication" /> - <interceptor-ref name="defaultStack"/> - </interceptor-stack> - <interceptor-stack name="<strong>user-submit</strong>" > - <interceptor-ref name="tokenSession" /> - <interceptor-ref name="user"/> - </interceptor-stack> - <interceptor-stack name="<strong>guest</strong>" > - <interceptor-ref name="defaultStack"/> - </interceptor-stack> -</interceptors> -<<strong>default-interceptor-ref</strong> name="user"/></code></pre> -<hr/> - -<p> - The <strong>user</strong> stacks require that the client be authenticated. - In other words, that a User object is present in the session. - The actions using a <strong>guest</strong> stack can be accessed by any client. - The <strong>-submit</strong> versions of each can be used with actions - with forms, to guard against double submits. -</p> - -<h5>Double Submits</h5> - -<p> - A common problem with designing web applications is that users are impatient - and response times can vary. - 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 checking out a shopping cart, - it's easier for a double submit to occur. -</p> - -<p> - To forestall double submits, and "back button" resubmits, - 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 that a form has been submitted twice or out of sequence. -</p> - -<p> - The Token Session Interceptor will also attempt to provide intelligent - fail-over in the event of multiple requests using the same session. - That is, it will block subsequent requests until the first request is complete, - and then instead of returning the "invalid.token" code, - it will attempt to display the same response that the - original, valid action invocation would have displayed -</p> - -<p> - Because the default interceptor stack will now authenticate the client, - we need to specify the standard "defaultStack" for the three - "guest actions", Welcome, Login, and Register. - Requiring authentification by default is the better practice, since it - means that we won't forget to enable it when creating new actions. - Meanwhile, those pesky users will ensure that we don't forget to disable - authentification for "guest" services. -</p> - -<h3><a name="MainMenu" id="MainMenu">MainMenu</a></h3> - -<p> - On a successful login, 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: -</p> - -<ul> - <li> - Edit your user registration profile - </li> - <li> - Log off MailReader Demonstration Application - </li> -</ul> - -<p> - Let's review the source for the "MainMenu" action mapping, - and the "MainMenu.jsp". -</p> - -<hr/> -<h5>Action mapping element for MainMenu</h5> -<pre><code><action name="MainMenu" class="mailreader2.MailreaderSupport"> - <result>/pages/MainMenu.jsp</result> - </action></code></pre> - -<h5>MainMenu.jsp</h5> -<pre><code><%@ page contentType="text/html; charset=UTF-8" %> -<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %> -<!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> - <title><s:text name="mainMenu.title"/></title> - <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet" - type="text/css"/> - </head> - - <body> - <h3><s:text name="mainMenu.heading"/> <strong><s:property - value="user.fullName"/></strong></h3> - <ul> - <li><a href="<s:url <strong>action="Registration_input"</strong> />"> - <s:text name="mainMenu.registration"/> - </a> - </li> - <li><a href="<s:url <strong>action="Logout"</strong> />"> - <s:text name="mainMenu.logout"/> - </a> - </ul> - </body> -</html></code></pre> -<hr/> - -<p> - The source for "MainMenu.jsp" also contains a new tag, <strong> - property</strong>, which we use to customize the page with the - "fullName" property of the authenticated user. -</p> - -<p> - Displaying the user's full name is the reason the MainMenu action - references the MailreaderSupport class. - The MailreaderSupport class has a User property that the text tag - can access. - If we did not utilize MailreaderSupport, - the property tag would not be able to find the User object to print - the full name. -</p> - -<p> - The customized MainMenu page offers two standard links. - One is to "Edit your user registration profile". - The other is to "Logout the MailReader Demonstration Application". -</p> - -<h3><a name="Registration.jsp" id="Registration.jsp">Registration page</a> -</h3> - -<p> - 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. -</p> - -<p> - To do double duty as the "Create" Registration page and the "Edit" - Registration page, - the "Registration.jsp" makes extensive use of the test tags, - to make it appears as though there are two distinct pages. -</p> - -<hr /> -<h5>Registration.jsp - head element</h5> -<pre><code><head> - <s:if test="<strong>task=='Create'</strong>"> - <title><s:text name="registration.title.create"/></title> - </s:if> - <s:if test="<strong>task=='Edit'</strong>"> - <title><s:text name="registration.title.edit"/></title> - </s:if> - <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet" - type="text/css"/> -</head></code></pre> -<hr /> - -<p> - For example, if client is editing the form (task == 'Edit'), - the page inserts the username from the User object. - For a new Registration (task == 'Create'), - the page creates an empty data-entry field. -</p> - -<hr/> -<h5>Note:</h5> -<blockquote> - <p><font class="hint"> - <strong>Presention Logic</strong> - - The "test" tag is a convenient way to express presentation - logic within your pages. - Customized pages help to prevent user error, - and dynamic customization reduces the number of server pages your - application needs to maintain, among other benefits. - </font></p> -</blockquote> -<hr/> - -<p> - 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. -</p> - -<hr/> -<h5></h5> -<pre><code><s:if test=<strong>"task == 'Edit'"</strong>> - <div align="center"> - <h3><s:text name="heading.subscriptions"/></h3> - </div> - <!-- ... --> - </s:if> -<jsp:include page="Footer.jsp"/> -</body></html></code></pre> -<hr/> - -<p> - Otherwise, the page contains just the top portion -- - a data-entry form for managing the user's registration. -</p> - -<h4><a name="iterator" id="iterator">iterator</a></h4> - -<p> - Besides "if" there are several other control tags that you can use - to sort, filter, or iterate over data. - The Registration page includes a good example of using the <strong>iterator</strong> - tag to display the User's Subscriptions. -</p> - -<p> - 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 iterator tag, you can code it the way it sounds. - </p> - -<hr/> -<h5>Using iterator to list the Subscriptions</h5> -<pre><code><s:iterator value="<strong>user.subscriptions</strong>"> - <tr> - <td align="left"> - <s:property value="<strong>host</strong>"/> - </td> - <td align="left"> - <s:property value="<strong>username</strong>"/> - </td> - <td align="center"> - <s:property value="<strong>type</strong>"/> - </td> - <td align="center"> - <s:property value="<strong>autoConnect</strong>"/> - </td> - <td align="center"> - <a href="<s:url action="<strong>Subscription_delete</strong>"><s:param name="<strong>host</strong>" value="host"/></s:url>"> - <s:text name="registration.deleteSubscription"/> - </a> - <a href="<s:url action="<strong>Subscription_edit</strong>"><s:param name="<strong>host</strong>" value="host"/></s:url>"> - <s:text name="registration.editSubscription"/> - </a> - </td> - </tr> -</s:iterator></code></pre> -<hr/> - -<p> - When the iterator renders, it generates a list of Subscriptions for the current User. -</p> - -<hr /> - - <div align="center"> - <h3>Current Subscriptions</h3> - </div> - - <table border="1" width="100%"> - <tr> - <th align="center" width="30%"> - Host Name - </th> - <th align="center" width="25%"> - User Name - </th> - - <th align="center" width="10%"> - Server Type - </th> - <th align="center" width="10%"> - Auto - </th> - <th align="center" width="15%"> - Action - </th> - </tr> - <tr> - <td align="left"> - mail.hotmail.com - </td> - <td align="left"> - user1234 - </td> - <td align="center"> - pop3 - </td> - - <td align="center"> - false - </td> - <td align="center"> - <a href="/struts2-mailreader/Subscription_delete.do?host=mail.hotmail.com"> - Delete - </a> - - <a href="/struts2-mailreader/Subscription_edit.do?host=mail.hotmail.com"> - Edit - </a> - </td> - </tr> - <tr> - <td align="left"> - mail.yahoo.com - </td> - <td align="left"> - jquser - </td> - <td align="center"> - imap - </td> - <td align="center"> - false - </td> - <td align="center"> - <a href="/struts2-mailreader/Subscription_delete.do?host=mail.yahoo.com"> - Delete - </a> - - <a href="/struts2-mailreader/Subscription_edit.do?host=mail.yahoo.com"> - Edit - </a> - </td> - </tr> - </table> - <a href="/struts2-mailreader/Subscription_input.do">Add</a> - -<hr /> - - <p> - Now look back at the code used to generate this block. - </p> - <p> - Notice anything nifty? - </p> - <p> - How about that the markup between the iterator tag is - actually <em>simpler</em> than the markup that we would use to render one row of the - table? - </p> - <p> - Instead of using a qualified reference like "value=user.subscription[0].host", - we use the simplest possible reference: "value=host". - We didn't have to define a local variable, and reference that local in the loop code. - The reference to each item in the list is automatically resolved, no fuss, no muss. - </p> - <p> - Nice trick! - </p> - -<p> - The secret to this magic is the <strong>value stack</strong>. - Next to Interceptors, the value stack is probably the coolest thing there is about the - framework. - To explain the value stack, let's step back and start from the beginning. -</p> - -<p> - Merging dynamic data into static web pages is a primary reason - we create web applications. - The Java API has a mechanism that allows you to - place objects in a servlet scope (page, request, session, or - application), and then retrieve them using a JSP scriplet. - If the object is placed directly in one of the scopes, - a JSP tag or scriptlet can find that object by searching page scope and - then request scope, and session scope, and finally application scope. -</p> - -<p> - The value stack works much the same way, only better. - When you push an object on the value stack, - the public properties of that object become first-class properties of the stack. - The object's properties become the stack's properties. - If another object on the stack has properties of the same name, - the last object pushed onto the stack wins. (Last-In, First-Out.) -</p> - -<p> - When the iterator tag loops through a collection, - it pushes each item in the collection onto the stack. - The item's properties become the stack's property. - In the case of the Subscriptions, - if the Subscription has a public Host property, - then during that iteration, - the stack can access the same property. -</p> - -<p> - Of course, at the end of each iteration, the tag "pops" the item off the stack. - If we were to try and access the Host property later in the page, - it won't be there. -</p> - -<p> - When an Action is invoked, the Action class is pushed onto the value stack. - Since the Action is on the value stack, - 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", - and the value stack returns the first property it finds by that name, - on any object on the stack. -</p> - -<p> - The Validators also use the stack. - When validation fails on a field, - the value for the field is pushed onto the value stack. - As a result, if the client enters text into an Integer field, - the framework can still redisplay whatever was entered. - An invalid input value is not stored in the field (even if it could be). - The invalid input is pushed onto the stack for the scope of the request. -</p> - -<p> - The Subscription list uses another new tag: the <strong>param</strong> tag. - As tags go, "param" takes very few parameters of its 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 "param" allows the other tags to do. -</p> - -<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}. -... -<s:text name="pager.legend"> - <s:<strong>param</strong> name="current" value="42" /> - <s:<strong>param</strong> name="count" value="314" /> - <s:<strong>param</strong> name="criteria" value="Life, the Universe, and Everything" /> -</s:text></code></pre> - -<p> - In the case of an "url" tag, - we can use "param" to create the query string. - A statement like this: -</p> - -<pre><code> - <s:url action="Subscription_edit"><s:param name="<strong>host" value="host</strong>"/></s:url>"> -</code></pre> - -<p> - can render a hyperlink like this: -</p> - -<pre><code> - <a href="/struts2-mailreader/Subscription_edit.do?<strong>host=mail.yahoo.com</strong>">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> ---> - -<p> - If a hyperlink needs more parameters, - you can use "param" to add as many parameters as needed. -</p> - -<h3> - <a name="Subscription" id="Subscription">Subscription</a> -</h3> - -<p> - 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 at the Subscription configuration - and follow the bouncing ball from page to action to page. -</p> - -<hr /> -<h5>mailreader-support.xml Subscription element</h5> -<pre><code><action name="Subscription_*" method="{1}" class="mailreader2.Subscription"> - <result name="input">/pages/Subscription.jsp</result> - <result type="redirect-action">Registration_input</result> -</action></code></pre> -<hr /> - -<p> - The Edit link specified the Subscription action, - but also includes the qualifier <strong>_edit</strong>. - The wildcard notation tells the framework to use any characters given after "Subscription_" - as the name of a method to invoke on the Action class, - instead of the default execute method. - The "alternate" execute methods are called <strong>alias</strong> methods. -</p> - -<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> - The "edit" alias has two responsibilities. - 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 relevant 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> - In the normal course, the Subscription should always be found, - since we selected the entry 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> - -<p> - The business logic for the "edit" alias is a simple wrapper - around the MailReader DAO classes. -</p> - -<hr /> -<h5>MailreaderSupport findSubscription()</h5> -<pre><code>public Subscription <strong>findSubscription()</strong> { - return findSubscription(getHost()); -} - -public Subscription findSubscription(String host) { - Subscription subscription; - subscription = <strong>getUser().findSubscription(host);</strong> - return subscription; -}</code></pre> -<hr /> - -<p> - 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 the - MailReader configuration will trap the exception for us. -</p> - -<p> - 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 Login page automatically. -</p> - -<p> - As a final layer of defense, we also configured a validator for Subscription, - to ensure that we are passed a Host parameter. -</p> - -<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> - By keeping routine safety precautions out of the Action class, - the all-important Action becomes smaller and easier to maintain. -</p> - -<p> - After setting the relevent Subscription object to the Subscription property, - the framework transfers control to the (you guessed it) Subscription page. -</p> - -<hr /> -<h5>Subscription.jsp</h5> -<pre><code><%@ page contentType="text/html; charset=UTF-8" %> -<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %> -<!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> - <s:if test="task=='Create'"> - <title><s:text name="subscription.title.create"/></title> - </s:if> - <s:if test="task=='Edit'"> - <title><s:text name="subscription.title.edit"/></title> - </s:if> - <s:if test="task=='Delete'"> - <title><s:text name="subscription.title.delete"/></title> - </s:if> - <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet" - type="text/css"/> - </head> - <body onLoad="self.focus();document.Subscription.username.focus()"> - - <s:actionerror/> - <s:form <strong>action="Subscription_save"</strong> validate="true"> - <strong><s:token /></strong> - <strong><s:hidden name="task"/></strong> - <strong><s:label key="username" name="user.username"/></strong> - - <s:if test="task == 'Create'"> - <s:textfield key="mailHostname" name="host"/> - </s:if> - <s:else> - <s:label key="mailHostname" name="host"/> - <s:hidden name="host"/> - </s:else> - - <s:if test="task == 'Delete'"> - <s:label key="subscription.username"/> - <s:label key="subscription.password"/> - <s:label key="subscription.type"/> - <s:label key="subscription.autoConnect"/> - <s:submit key="button.confirm"/> - </s:if> - <s:else> - <s:textfield key="subscription.username"/> - <s:textfield key="subscription.password"/> - <strong><s:select key="subscription.type" list="types"/></strong> - <strong><s:checkbox key="subscription.autoConnect"/></strong> - <s:submit key="button.save"/> - <s:reset key="button.reset"/> - </s:else> - - <s:submit action="Registration_input" - key="button.cancel" - onclick="form.onsubmit=null"/> - </s:form> - - <jsp:include page="Footer.jsp"/> - </body> -</html></code></pre> -<hr /> - -<p> - As before, we'll discuss the tags and attributes that are new to this page: - "token", "hidden", "label", "select", and "checkbox". -</p> - -<p> - 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> - -<p> - The <strong>hidden</strong> tag embeds the Task property into the form. - When the form is submitted, - the Subscription_save action will use the Task property to decide - whether to insert or update the form. -</p> - -<p> - 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> - Saving the best for last, the Subscription form utilizes two more interesting - tags, "select" and "checkbox". -</p> - -<p> - Unsurprisingly, the <strong>select</strong> tag renders a select control, - but the tag does so without requiring a lot of markup or redtape. -</p> - -<pre><code><s:select key="subscription.type" <strong>list="types"</strong> /> -</code></pre> - -<p> - 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> - The default Interceptor stack includes the <strong>PrepareInterceptor</strong>, - which observes the Preparable interface. -</p> - -<hr /> -<h5>PrepareInterceptor</h5> -<pre><code>public class <strong>PrepareInterceptor</strong> extends AroundInterceptor { - - protected void after(ActionInvocation dispatcher, String result) throws Exception { - } - - protected void before(ActionInvocation invocation) throws Exception { - Object action = invocation.getAction(); - <strong>if (action instanceof Preparable) { - ((Preparable) action).prepare();</strong> - } - } -}</code></pre> - -<p> - 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> - -<h4> - <a name="Subscription.java" id="Subscription.java">Subscription.java</a> -</h4> - -<p> - Like many applications, the MailReader uses mainly String properties. - One exception is the AutoConnect property of the Subscription object. - On the HTML form, the AutoConnect property is represented by a checkbox. -</p> - -<p> - When writing web applications, the checkbox can be a tricky control. - 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> - -<p> - In Struts 1, - we use the <code>reset</code> method to work around checkbox issues. - In Struts 2, checkbox state is handled automatically. - The framework can detect when a checkbox tag has not been sent back, - and when that happens, - a default "false" value is used for the checkbox value. - No worries, mate. -</p> - -<p> - If we press the SAVE button, - the form will be submitted to the Subscription_save action. - Since the save method needs some additional validation, - we can add a validation file. -</p> - -<hr /> -<h5>Subscription-Subscription_save-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> - The validators follow the same type of inheritance path as the classes. - SubscriptionSave extends Subscription, - so when Subscription_save is validated, - the Host property specified by "Subscription-validation.xml" will also be required. -</p> - -<p> - If validation succeeds, the <code>save</code> method of Subscription will fire. -</p> - -<hr /> -<h5>Subscription</h5> - -<pre><code>public String <strong>save</strong>() throws Exception { - - if (Constants.DELETE.equals(getTask())) { - <strong>removeSubscription</strong>(); - } - - if (Constants.CREATE.equals(getTask())) { - <strong>copySubscription(</strong>getHost()); - } - - saveUser(); - return SUCCESS; -}</code></pre> -<hr /> - -<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> - -<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 <strong>removeSubscription</strong>() throws Exception { - getUser().removeSubscription(getSubscription()); - getSession().remove(Constants.SUBSCRIPTION_KEY); -}</code></pre> -<hr /> - -<p> - The <strong>copySubscription</strong> method is a bit more interesting. - 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 then save it when we are done -- because some fields can't be populated, - except at construction. -</p> - -<p> - One 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 could pass the new object values from the Action properties. -</p> - -<p> - Another workaround is to declare only the immutable properties on the Action, - and then use what we can from the domain object. -</p> - -<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> - -<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> - -<hr /> -<h5>copySubscription</h5> -<pre><code>public void <strong>copySubscription</strong>(String host) { - Subscription input = getSubscription(); - Subscription sub = createSubscription(host); - if (null != sub) { - <strong>BeanUtils.setValues</strong>(sub, input, null); - setSubscription(sub); - setHost(sub.getHost()); - } -}</code></pre> -<hr /> - -<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> - -<h4>Subscription Submit</h4> - -<p> - When we pressed the SAVE button, there was one step that we overlooked. - The Mailreader application uses a "double submit" guard to keep people - from clicking the SAVE button multiple times and submitting the form again. -</p> - -<p> - To add the double-submit guard, we can change the actions default processing - stack to <code>user-submit</code>. - But, we don't want to just copy and paste the other action settings from - the main Subscription action. - What we can do is put the subscription actions in their own package, - so that they can share result types. -</p> - -<hr /> -<h5>mailreader-support.xml</h5> -<pre><code><!-- ... --> -</package> - -<package name="subscription" namespace="/" extends="mailreader-support"> - - <global-results> - <result name="input">/Subscription.jsp</result> - <result type="redirect-action">Registration_input</result> - </global-results> - - <action name="Subscription_save" method="save" class="mailreader2.Subscription"> - <interceptor-ref name="user-submit" /> - </action> - - <action name="Subscription_*" method="{1}" class="mailreader2.Subscription" /> - -</package> - -<package name="wildcard" namespace="/" extends="mailreader-support"> - - <action name="*" class="mailreader2.MailreaderSupport"> - <result>/{1}.jsp</result> - </action> - -</package> -}</code></pre> -<hr /> - -<p> - Aftering a successful save, - the Subscription Action will return "success", - and the framework will redirect us back to Registration input. -</p> - -<h3>Summary</h3> -<p> - At this point, we've booted the application, logged on, - reviewed a Registration 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/viewvc/struts/struts2/trunk/apps/mailreader/"> - available online</a> - and in the distribution. -</p> - -<p> - Enjoy! -</p> -</blockquote> -</body> -</html>
http://git-wip-us.apache.org/repos/asf/struts/blob/fa9cac70/apps/pom.xml ---------------------------------------------------------------------- diff --git a/apps/pom.xml b/apps/pom.xml index 91b759c..87de124 100644 --- a/apps/pom.xml +++ b/apps/pom.xml @@ -32,7 +32,6 @@ <packaging>pom</packaging> <name>Struts 2 Webapps</name> <modules> - <module>mailreader</module> <module>portlet</module> <module>showcase</module> <module>rest-showcase</module>