http://git-wip-us.apache.org/repos/asf/struts-examples/blob/0677ec5c/mailreader/src/main/webapp/tour.html ---------------------------------------------------------------------- diff --git a/mailreader/src/main/webapp/tour.html b/mailreader/src/main/webapp/tour.html new file mode 100644 index 0000000..7dc29f0 --- /dev/null +++ b/mailreader/src/main/webapp/tour.html @@ -0,0 +1,2470 @@ +<!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-examples/blob/0677ec5c/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 0965639..0000000 --- a/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/* - * $Id$ - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - <parent> - <groupId>org.apache.struts</groupId> - <artifactId>struts2-apps</artifactId> - <version>2.5-SNAPSHOT</version> - </parent> - - <artifactId>struts2-mailreader</artifactId> - <packaging>war</packaging> - <name>Struts 2 Mail Reader Webapp</name> - - <dependencies> - - <dependency> - <groupId>javax.servlet</groupId> - <artifactId>servlet-api</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>struts-mailreader-dao</artifactId> - <version>1.3.5</version> - </dependency> - - <!-- Logging --> - <dependency> - <groupId>org.apache.logging.log4j</groupId> - <artifactId>log4j-core</artifactId> - <version>${log4j2.version}</version> - </dependency> - <dependency> - <groupId>org.apache.logging.log4j</groupId> - <artifactId>log4j-jcl</artifactId> - <version>${log4j2.version}</version> - </dependency> - - </dependencies> - - <build> - <resources> - <resource> - <directory>src/main/java</directory> - <includes> - <include>**/*.xml</include> - <include>**/*.properties</include> - </includes> - </resource> - </resources> - <plugins> - <plugin> - <groupId>org.mortbay.jetty</groupId> - <artifactId>jetty-maven-plugin</artifactId> - <version>8.1.16.v20140903</version> - <configuration> - <stopKey>CTRL+C</stopKey> - <stopPort>8999</stopPort> - <scanIntervalSeconds>10</scanIntervalSeconds> - <webAppSourceDirectory>${basedir}/src/main/webapp/</webAppSourceDirectory> - <webAppConfig> - <contextPath>/struts2-mailreader</contextPath> - <descriptor>${basedir}/src/main/webapp/WEB-INF/web.xml</descriptor> - </webAppConfig> - </configuration> - </plugin> - </plugins> - </build> - <properties> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - </properties> -</project>