diff --git a/apps/mailreader/README.txt b/apps/mailreader/README.txt
new file mode 100644
index 000000000..cdb3df847
--- /dev/null
+++ b/apps/mailreader/README.txt
@@ -0,0 +1,15 @@
+README.txt - mailreader-bang
+
+This application demonstrates switching from the "bang" syntax for invoking
+dynamic methods to a general-purpose wild card approach.
+
+To switch between approaches, edit the struts.xml file to include either the
+struts-bang.xml file OR the struts-wildcard.xml. (But not both.)
+
+When using the -bang application, be sure that the
+struts.enable.DynamicMethodInvocation property is set to "true".
+
+For the -wilcard application. be sure that the
+struts.enable.DynamicMethodInvocation property is set to "false".
+
+----------------------------------------------------------------------------
\ No newline at end of file
diff --git a/apps/mailreader/pom.xml b/apps/mailreader/pom.xml
new file mode 100644
index 000000000..f4915e2e9
--- /dev/null
+++ b/apps/mailreader/pom.xml
@@ -0,0 +1,49 @@
+
+
ServletContextListener that initializes and finalizes the
+ * persistent storage of User and Subscription information for the Struts
+ * Demonstration Application, using an in-memory database backed by an XML
+ * file.
IMPLEMENTATION WARNING - If this web application is run + * from a WAR file, or in another environment where reading and writing of the + * web application resource is impossible, the initial contents will be copied + * to a file in the web application temporary directory provided by the + * container. This is for demonstration purposes only - you should + * NOT assume that files written here will survive a restart + * of your servlet container.
+ * + *This class was borrowed from the Shale Mailreader. Changes were:
+ * + *+ * DEVELOPMENT NOTE - Another approach would be to instantiate the database via Spring. + *
+ */ + +public final class ApplicationListener implements ServletContextListener { + + // ------------------------------------------------------ Manifest Constants + + + /** + *Appication scope attribute key under which the in-memory version of + * our database is stored.
+ */ + public static final String DATABASE_KEY = "database"; + + + /** + *Application scope attribute key under which the valid selection + * items for the protocol property is stored.
+ */ + public static final String PROTOCOLS_KEY = "protocols"; + + // ------------------------------------------------------ Instance Variables + + + /** + *The ServletContext for this web application.
Logging output for this plug in instance.
+ */ + private Log log = LogFactory.getLog(this.getClass()); + + // ------------------------------------------------------------- Properties + + + /** + *The web application resource path of our persistent database storage + * file.
+ */ + private String pathname = "/WEB-INF/classes/database.xml"; + + /** + *Return the application resource path to the database.
+ * + * @return application resource path path to the database + */ + public String getPathname() { + return (this.pathname); + } + + /** + *Set the application resource path to the database.
+ * + * @param pathname to the database + */ + public void setPathname(String pathname) { + this.pathname = pathname; + } + + // ------------------------------------------ ServletContextListener Methods + + + /** + *Gracefully shut down this database, releasing any resources that + * were allocated at initialization.
+ * + * @param event ServletContextEvent to process + */ + public void contextDestroyed(ServletContextEvent event) { + + log.info("Finalizing memory database plug in"); + + if (database != null) { + try { + database.close(); + } catch (Exception e) { + log.error("Closing memory database", e); + } + } + + context.removeAttribute(DATABASE_KEY); + context.removeAttribute(PROTOCOLS_KEY); + database = null; + context = null; + + } + + + /** + *Initialize and load our initial database from persistent + * storage.
+ * + * @param event The context initialization event + */ + public void contextInitialized(ServletContextEvent event) { + + log.info("Initializing memory database plug in from '" + + pathname + "'"); + + // Remember our associated ServletContext + this.context = event.getServletContext(); + + // Construct a new database and make it available + database = new MemoryUserDatabase(); + try { + String path = calculatePath(); + if (log.isDebugEnabled()) { + log.debug(" Loading database from '" + path + "'"); + } + database.setPathname(path); + database.open(); + } catch (Exception e) { + log.error("Opening memory database", e); + throw new IllegalStateException("Cannot load database from '" + + pathname + "': " + e); + } + context.setAttribute(DATABASE_KEY, database); + + } + + // -------------------------------------------------------- Private Methods + + + /** + *Calculate and return an absolute pathname to the XML file to contain + * our persistent storage information.
+ * + * @throws Exception if an input/output error occurs + */ + private String calculatePath() throws Exception { + + // Can we access the database via file I/O? + String path = context.getRealPath(pathname); + if (path != null) { + return (path); + } + + // Does a copy of this file already exist in our temporary directory + File dir = (File) + context.getAttribute("javax.servlet.context.tempdir"); + File file = new File(dir, "struts-example-database.xml"); + if (file.exists()) { + return (file.getAbsolutePath()); + } + + // Copy the static resource to a temporary file and return its path + InputStream is = + context.getResourceAsStream(pathname); + BufferedInputStream bis = new BufferedInputStream(is, 1024); + FileOutputStream os = + new FileOutputStream(file); + BufferedOutputStream bos = new BufferedOutputStream(os, 1024); + byte buffer[] = new byte[1024]; + while (true) { + int n = bis.read(buffer); + if (n <= 0) { + break; + } + bos.write(buffer, 0, n); + } + bos.close(); + bis.close(); + return (file.getAbsolutePath()); + + } + + +} diff --git a/apps/mailreader/src/main/java/mailreader2/AuthenticationInterceptor.java b/apps/mailreader/src/main/java/mailreader2/AuthenticationInterceptor.java new file mode 100644 index 000000000..10cc36823 --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/AuthenticationInterceptor.java @@ -0,0 +1,31 @@ +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 AuthenticationInterceptor implements Interceptor { + + public void destroy () {} + + public void init() {} + + public String intercept(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 (!isAuthenticated) { + return Action.LOGIN; + } + else { + return actionInvocation.invoke(); + } + + } +} diff --git a/apps/mailreader/src/main/java/mailreader2/Constants.java b/apps/mailreader/src/main/java/mailreader2/Constants.java new file mode 100644 index 000000000..89d96075a --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/Constants.java @@ -0,0 +1,125 @@ +/* + * $Id: Constants.java 360442 2005-12-31 20:10:04Z husted $ + * + * Copyright 1999-2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package mailreader2; + +/** + *Manifest constants for the MailReader application.
+ */ +public final class Constants { + + // --- Tokens ---- + + /** + *The token representing a "cancel" request.
+ */ + public static final String CANCEL = "cancel"; + + /** + *The token representing a "create" task.
+ */ + public static final String CREATE = "Create"; + + /** + *The application scope attribute under which our user database is + * stored.
+ */ + public static final String DATABASE_KEY = "database"; + + /** + *The token representing a "edit" task.
+ */ + public static final String DELETE = "Delete"; + + /** + *The token representing a "edit" task.
+ */ + public static final String EDIT = "Edit"; + + /** + *The package name for this application.
+ */ + public static final String PACKAGE = "org.apache.struts.apps.mailreader"; + + /** + *The session scope attribute under which the Subscription object + * currently selected by our logged-in User is stored.
+ */ + public static final String SUBSCRIPTION_KEY = "subscription"; + + /** + *The session scope attribute under which the User object for the + * currently logged in user is stored.
+ */ + public static final String USER_KEY = "user"; + + /** + *The token representing the "Host" property. + */ + public static final String HOST = "host"; + + + // ---- Error Messages ---- + + /** + *
+ * A static message in case message resource is not loaded. + *
+ */ + public static final String ERROR_MESSAGES_NOT_LOADED = + "ERROR: Message resources not loaded -- check servlet container logs for error messages."; + + /** + *+ * A static message in case database resource is not loaded. + *
+ */ + public static final String ERROR_DATABASE_NOT_LOADED = + "ERROR: User database not loaded -- check servlet container logs for error messages."; + + /** + *
+ * A standard key from the message resources file, to test if it is available. + *
+ */ + public static final String ERROR_DATABASE_MISSING = "error.database.missing"; + + /** + *
+ * A "magic" username to trigger an ExpiredPasswordException for testing. + *
+ */ + public static final String EXPIRED_PASSWORD_EXCEPTION = "ExpiredPasswordException"; + + /** + *+ * Name of field to associate with authentification errors. + *
+ */ + public static final String PASSWORD_MISMATCH_FIELD = "password"; + + // ---- Log Messages ---- + + /** + *
Message to log if saving a user fails.
+ */ + public static final String LOG_DATABASE_SAVE_ERROR = + " Unexpected error when saving User: "; + + +} diff --git a/apps/mailreader/src/main/java/mailreader2/Logoff.java b/apps/mailreader/src/main/java/mailreader2/Logoff.java new file mode 100644 index 000000000..4cbafa55a --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/Logoff.java @@ -0,0 +1,32 @@ +/* + * $Id: LogonAction.java 360442 2005-12-31 20:10:04Z husted $ + * + * Copyright 2000-2004 Apache Software Foundation + * + * Licensed 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. + */ + +package mailreader2; + +/** + *Log user out of the current session.
+ */ +public class Logoff extends MailreaderSupport { + + public String execute() { + + setUser(null); + + return SUCCESS; + } +} diff --git a/apps/mailreader/src/main/java/mailreader2/Logon-validation.xml b/apps/mailreader/src/main/java/mailreader2/Logon-validation.xml new file mode 100644 index 000000000..4a04c7629 --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/Logon-validation.xml @@ -0,0 +1,14 @@ + + +Validate a user logon.
+ */ +public final class Logon extends MailreaderSupport { + + public String execute() throws ExpiredPasswordException { + + User user = findUser(getUsername(), getPassword()); + + if (user != null) { + setUser(user); + } + + if (hasErrors()) { + return INPUT; + } + + return SUCCESS; + + } + +} diff --git a/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.java b/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.java new file mode 100644 index 000000000..71e971c04 --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.java @@ -0,0 +1,559 @@ +/* + * $Id: BaseAction.java 360442 2005-12-31 20:10:04Z husted $ + * + * Copyright 1999-2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package mailreader2; + +import com.opensymphony.util.BeanUtils; +import org.apache.struts2.interceptor.ApplicationAware; +import org.apache.struts2.interceptor.SessionAware; +import com.opensymphony.xwork2.ActionSupport; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.struts.apps.mailreader.dao.ExpiredPasswordException; +import org.apache.struts.apps.mailreader.dao.Subscription; +import org.apache.struts.apps.mailreader.dao.User; +import org.apache.struts.apps.mailreader.dao.UserDatabase; +import org.apache.struts.apps.mailreader.dao.impl.memory.MemorySubscription; +import org.apache.struts.apps.mailreader.dao.impl.memory.MemoryUser; + +import java.util.Map; + +/** + *Base Action for MailreaderSupport application.
+ * + *Note that this class does NOT implement model driven because of the way + * the pre-existing model is designed. The MailReader DAO includes immutable + * fields that can only be set on construction, and some objects do not have a + * default construction. One approach would be to mirror all the DAO + * properties on the Actions. As an alternative, this implementations uses the + * DAO properties where possible, and uses local Action properties only as + * needed. To create new objects, a blank temporary object is constructed, and + * the page uses a mix of local Action properties and DAO properties. When the + * new object is to be saved, the local Action properties are used to create + * the object using the DAO factory methods, the input values are copied from + * the temporary object, and the new object is saved. It's kludge, but it + * avoids creating unnecessary local properties. Pick your poison.
+ */ +public class MailreaderSupport extends ActionSupport + implements SessionAware, ApplicationAware { + + /** + * Return CANCEL so apropriate result can be selected. + * @return "cancel" so apropriate result can be selected. + */ + public String cancel() { + return Constants.CANCEL; + } + + + // ---- ApplicationAware ---- + + /** + *Field to store application context or its proxy.
+ * + *The application context lasts for the life of the application. A + * reference to the database is stored in the application context at + * startup.
+ */ + private Map application; + + /** + *Store a new application context.
+ * + * @param value A Map representing application state + */ + public void setApplication(Map value) { + application = value; + } + + /** + *Provide application context.
+ */ + public Map getApplication() { + return application; + } + + // ---- SessionAware ---- + + /** + *Field to store session context, or its proxy.
+ */ + private Map session; + + /** + *Store a new session context.
+ * + * @param value A Map representing session state + */ + public void setSession(Map value) { + session = value; + } + + /** + *Provide session context.
+ * + * @return session context + */ + public Map getSession() { + return session; + } + + // ---- Task property (utilized by UI) ---- + + /** + *Field to store workflow task.
+ * + *The Task is used to track the state of the CRUD workflows. It can be + * set to Constant.CREATE, Constant.EDIT, or Constant.DELETE as + * needed.
+ */ + private String task = null; + + + /** + *Provide worklow task.
+ * + * @return Returns the task. + */ + public String getTask() { + return task; + } + + /** + *Store new workflow task.
+ * + * @param value The task to set. + */ + public void setTask(String value) { + task = value; + } + + // ---- Token property (utilized by UI) ---- + + /** + *Field to store double-submit guard.
+ */ + private String token = null; + + + /** + *Provide Token.
+ * + * @return Returns the token. + */ + public String getToken() { + return token; + } + + /** + *Store new Token.
+ * + * @param value The token to set. + */ + public void setToken(String value) { + token = value; + } + + + // ---- Host property ---- + + /** + *Field to store Subscription host.
+ * + *The host is an immutable property of the Subscrtion DAP object, so + * we need to store it locally until we are ready to create the + * Subscription.
+ */ + private String host; + + /** + *Provide tSubscription host.
+ * + * @return host property + */ + public String getHost() { + return host; + } + + /** + *Store new Subscription host.
+ * + * @param value + */ + public void setHost(String value) { + host = value; + } + + // ---- Password property ---- + + /** + *Field to store User password property.
+ * + *The User DAO object password proerty is immutable, so we store it + * locally until we are ready to create the object.
+ */ + private String password = null; + + + /** + *Provide User password
+ * + * @return Returns the password. + */ + public String getPassword() { + return password; + } + + /** + *Store new User Password
+ * + * @param value The password to set. + */ + public void setPassword(String value) { + password = value; + } + + // ---- Password2 property (confirmation) ---- + + /** + *Field to store the User password confirmation.
+ * + *When a User object is created, we ask the client to enter the + * password twice, to help ensure the password is being typed + * correctly.
+ */ + private String password2 = null; + + + /** + *Provide the User password confirmation.
+ * + * @return Returns the confirmationpassword. + */ + public String getPassword2() { + return password2; + } + + /** + *Store a new User password confirmation.
+ * + * @param value The confirmation password to set. + */ + public void setPassword2(String value) { + password2 = value; + } + + // ---- Username property ---- + + /** + *Field to store User username.
+ * + *The User DAO object password proerty is immutable, so we store it + * locally until we are ready to create the object.
+ */ + private String username = null; + + + /** + *Provide User username.
+ * + * @return Returns the User username. + */ + public String getUsername() { + return username; + } + + /** + *Store new User username
+ * + * @param value The username to set. + */ + public void setUsername(String value) { + username = value; + } + + // ---- Database property ---- + + /** + *Provide reference to UserDatabase, or null if the database is not + * available.
+ * + * @return a reference to the UserDatabase or null if the database is not + * available + */ + public UserDatabase getDatabase() { + Object db = getApplication().get(Constants.DATABASE_KEY); + if (db == null) { + this.addActionError(getText("error.database.missing")); + } + return (UserDatabase) db; + } + + /** + *Store a new reference to UserDatabase
+ * + * @param database + */ + public void setDatabase(UserDatabase database) { + getApplication().put(Constants.DATABASE_KEY, database); + } + + // ---- User property ---- + + /** + *Provide reference to User object for authenticated user.
+ * + * @return User object for authenticated user. + */ + public User getUser() { + return (User) getSession().get(Constants.USER_KEY); + } + + /** + *Store new reference to User Object.
+ * + * @param user User object for authenticated user + */ + public void setUser(User user) { + getSession().put(Constants.USER_KEY, user); + } + + /** + *Obtain User object from database, or return null if the credentials + * are not found or invalid.
+ * + * @param username User username + * @param password User password + * @return User object or null if not found + * @throws ExpiredPasswordException + */ + public User findUser(String username, String password) + throws ExpiredPasswordException { + // FIXME: Stupid testing hack to compensate for inadequate DAO layer + if (Constants.EXPIRED_PASSWORD_EXCEPTION.equals(username)) { + throw new ExpiredPasswordException(Constants.EXPIRED_PASSWORD_EXCEPTION); + } + + User user = getDatabase().findUser(username); + if ((user != null) && !user.getPassword().equals(password)) { + user = null; + } + if (user == null) { + this.addFieldError(Constants.PASSWORD_MISMATCH_FIELD, + getText("error.password.mismatch")); + } + return user; + } + + /** + *Log instance for this application.
Persist the User object, including subscriptions, to the database. + *
+ * + * @throws java.lang.Exception on database error + */ + public void saveUser() throws Exception { + try { + getDatabase().save(); + } catch (Exception e) { + String message = Constants.LOG_DATABASE_SAVE_ERROR + getUser() + .getUsername(); + log.error(message, e); + throw new Exception(message, e); + } + } + + public void createInputUser() { + User user = new MemoryUser(null, null); + setUser(user); + } + + /** + *Verify input for creating a new user, create the user, and process + * the login.
+ * + * @return A new User and empty Errors if create succeeds, or null and + * Errors if create fails + */ + public User createUser(String username, String password) { + + UserDatabase database = getDatabase(); + User user; + + try { + user = database.findUser(username); + } + + catch (ExpiredPasswordException e) { + user = getUser(); // Just so that it is not null + } + + if (user != null) { + this.addFieldError("username", "error.username.unique"); + return null; + } + + return database.createUser(username); + } + + // Since user.username is immutable, we have to use some local properties + + /** + *Use the current User object to create a new User object, and make + * the new User object the authenticated user.
+ * + *The "current" User object is usually a temporary object being used + * to capture input.
+ * + * @param _username User username + * @param _password User password + */ + public void copyUser(String _username, String _password) { + User input = getUser(); + input.setPassword(_password); + User user = createUser(_username, _password); + if (null != user) { + BeanUtils.setValues(user, input, null); + setUser(user); + } + } + + // ---- Subscription property ---- + + /** + *Obtain the cached Subscription object, if any.
+ * + * @return Cached Subscription object or null + */ + public Subscription getSubscription() { + return (Subscription) getSession().get(Constants.SUBSCRIPTION_KEY); + } + + /** + *Store new User Subscription.
+ * + * @param subscription + */ + public void setSubscription(Subscription subscription) { + getSession().put(Constants.SUBSCRIPTION_KEY, subscription); + } + + /** + *Obtain User Subscription object for the given host, or return null + * if not found.
+ * + *It would be possible for this code to throw a NullPointerException, + * but the ExceptionHandler in the xwork.xml will catch that for us.
+ * + * @return The matching Subscription or null + */ + public Subscription findSubscription(String host) { + Subscription subscription; + subscription = getUser().findSubscription(host); + return subscription; + } + + /** + *Obtain uSER Subscription for the local Host property.
+ * + *Usually, the host property will be set from the client request, + * because it was embedded in a link to the Subcription action. + * + * @return Subscription or null if not found + */ + public Subscription findSubscription() { + return findSubscription(getHost()); + } + + /** + *
Provide a "temporary" User Subscription object that can be used to + * capture input values.
+ */ + public void createInputSubscription() { + Subscription sub = new MemorySubscription(getUser(), null); + setSubscription(sub); + setHost(sub.getHost()); + } + + /** + *Provide new User Subscription object for the given host, or null if + * the host is not unique.
+ * + * @param host + * @return New User Subscription object or null + */ + public Subscription createSubscription(String host) { + + Subscription sub; + + sub = findSubscription(host); + + if (null != sub) { + // FIXME - localization - "error.host.unique") + addFieldError(Constants.HOST,"That hostname is already defined"); + return null; + } + + return getUser().createSubscription(host); + } + + /** + *Create a new Subscription from the current Subscription object, + * making the new Subscription the current Subscription.
+ * + *Usually, the "current" Subscription is a temporary object being used + * to capture input values.
+ * + * @param host + */ + public void copySubscription(String host) { + Subscription input = getSubscription(); + Subscription sub = createSubscription(host); + if (null != sub) { + BeanUtils.setValues(sub, input, null); + setSubscription(sub); + setHost(sub.getHost()); + } + } + + /** + *Delete the current Subscription object from the database.
+ */ + public void removeSubscription() { + getUser().removeSubscription(getSubscription()); + getSession().remove(Constants.SUBSCRIPTION_KEY); + } + + /** + *Provide MailServer Host for current User Subscription.
+ * + * @return MailServer Host for current User Subscription + */ + public String getSubscriptionHost() { + Subscription sub = getSubscription(); + if (null == sub) { + return null; + } + return sub.getHost(); + } + +} diff --git a/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.properties b/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.properties new file mode 100644 index 000000000..df33d47cc --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.properties @@ -0,0 +1,93 @@ +button.cancel=Cancel +button.confirm=Confirm +button.doSubmit=DO_SUBMIT +button.doReset=DO_RESULT +button.doCancel=org.apache.struts.taglib.html.CANCEL +button.reset=Reset +button.save=Save +change.message=Your password has expired. Please ask the system administrator to change it. +change.try=Try Again +change.title=Password Has Expired +database.load=Cannot load database from {0} +error.database.missing=User database is missing, cannot validate logon credentials +error.fromAddress.format=Invalid format for From Address +error.fromAddress.required=From Address is required +error.fullName.required=Full Name is required +error.host.required=Mail Server is required +error.noSubscription=No Subscription bean in user session +error.password.expired=Your password has expired for username {0} +error.password.required=Password is required +error.password2.required=Confirmation password is required +error.password.match=Password and confirmation password must match +error.password.mismatch=Invalid username and/or password, please try again +error.replyToAddress.format=Invalid format for Reply To Address +struts.messages.invalid.token=Cannot submit this form out of order +error.type.invalid=Server Type must be 'imap' or 'pop3' +error.type.required=Server Type is required +error.username.required=Username is required +error.username.unique=That username is already in use - please select another +errors.footer=You must correct the following error(s) before proceeding:
\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044:
\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u043d\u0438\u0436\u0435 \u043e\u0448\u0438\u0431\u043a\u0438:
Insert or update a User object to the persistent store.
+ */ +public class Registration extends MailreaderSupport { + + /** + *Double check that there is not a valid User logon.
+ * + * @return True if there is not a valid User logon + */ + private boolean isCreating() { + User user = getUser(); + return (null == user) || (null == user.getDatabase()); + } + + /** + *Retrieve User object to edit or null if User does not exist.
+ * + * @return The "Success" result for this mapping + * @throws Exception on any error + */ + public String input() throws Exception { + + if (isCreating()) { + createInputUser(); + setTask(Constants.CREATE); + } else { + setTask(Constants.EDIT); + setUsername(getUser().getUsername()); + setPassword(getUser().getPassword()); + setPassword2(getUser().getPassword()); + } + + return INPUT; + } + + /** + *Insert or update a User object to the persistent store.
+ * + *If a User is not logged in, then a new User is created and + * automatically logged in. Otherwise, the existing User is updated.
+ * + * @return The "Success" result for this mapping + * @throws Exception on any error + */ + public String execute() + throws Exception { + + boolean creating = Constants.CREATE.equals(getTask()); + creating = creating && isCreating(); // trust but verify + + if (creating) { + + User user = findUser(getUsername(), getPassword()); + boolean haveUser = (user != null); + + if (haveUser) { + addActionError(getText("error.username.unique")); + return INPUT; + } + + copyUser(getUsername(), getPassword()); + + } else { + + // FIXME: Any way to call the RegisrationSave validators from here? + String newPassword = getPassword(); + if (newPassword != null) { + String confirmPassword = getPassword2(); + boolean matches = ((null != confirmPassword) + && (confirmPassword.equals(newPassword))); + if (matches) { + getUser().setPassword(newPassword); + } else { + addActionError(getText("error.password.match")); + return INPUT; + } + } + } + + saveUser(); + + return SUCCESS; + } + +} diff --git a/apps/mailreader/src/main/java/mailreader2/Subscription-validation.xml b/apps/mailreader/src/main/java/mailreader2/Subscription-validation.xml new file mode 100644 index 000000000..df903c25b --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/Subscription-validation.xml @@ -0,0 +1,11 @@ + + +Provide an Edit method for retrieving an existing subscription, and a + * Save method for updating or inserting a subscription.
+ */ +public class Subscription extends MailreaderSupport + implements Preparable { + + /** + *Field to store list of MailServer types
+ */ + private Map types = null; + + /** + *Provide the list of MailServer types.
+ * + * @return List of MailServer types + */ + public Map getTypes() { + return types; + } + + /** + *Setup the MailerServer types and set the local Host property from + * the User Subscription (if any).
+ */ + public void prepare() { + + Map m = new LinkedHashMap(); + m.put("imap", "IMAP Protocol"); + m.put("pop3", "POP3 Protocol"); + types = m; + + setHost(getSubscriptionHost()); + } + + /** + *Setup a temporary User Subscription object to capture input + * values.
+ * + * @return INPUT + */ + public String input() { + createInputSubscription(); + setTask(Constants.CREATE); + return INPUT; + } + + /** + *Load User Subscription for the local Host property.
+ * + *Usually, the Host is being set from the request by a link to an Edit + * or Delete task.
+ * + * @return INPUT or Error, if Subscription is not found + */ + public String find() { + + org.apache.struts.apps.mailreader.dao.Subscription + sub = findSubscription(); + + if (sub == null) { + return ERROR; + } + + setSubscription(sub); + + return INPUT; + + } + + /** + *Prepare to present a confirmation page before removing + * Subscription.
+ * + * @return INPUT or Error, if Subscription is not found + */ + public String delete() { + + setTask(Constants.DELETE); + return find(); + } + + /** + *Prepare to edit User Subscription.
+ * + * @return INPUT or Error, if Subscription is not found + */ + public String edit() { + + setTask(Constants.EDIT); + return find(); + } + + /** + *Examine the Task property and DELETE, CREATE, or save the User + * Subscription, as appropriate.
+ * + * @return SUCCESS + * @throws Exception on a database error + */ + public String save() throws Exception { + + if (Constants.DELETE.equals(getTask())) { + removeSubscription(); + } + + if (Constants.CREATE.equals(getTask())) { + copySubscription(getHost()); + } + + if (hasErrors()) return INPUT; + + saveUser(); + return SUCCESS; + } + +} diff --git a/apps/mailreader/src/main/java/mailreader2/SubscriptionSave-validation.xml b/apps/mailreader/src/main/java/mailreader2/SubscriptionSave-validation.xml new file mode 100644 index 000000000..9f2f6d793 --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/SubscriptionSave-validation.xml @@ -0,0 +1,23 @@ + + +Save a subscriptioin, resetting checkbox in prepare.
+ */ +public final class SubscriptionSave extends Subscription { + + public void prepare() { + super.prepare(); + // checkbox workaround + getSubscription().setAutoConnect(false); + } + + public String execute() throws Exception { + return save(); + } + + + +} diff --git a/apps/mailreader/src/main/java/mailreader2/Welcome.java b/apps/mailreader/src/main/java/mailreader2/Welcome.java new file mode 100644 index 000000000..08ec12581 --- /dev/null +++ b/apps/mailreader/src/main/java/mailreader2/Welcome.java @@ -0,0 +1,28 @@ +package mailreader2; + +/** + * Verify that essential resources are available. + */ +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)) { + addActionError(Constants.ERROR_MESSAGES_NOT_LOADED); + } + + // Confirm database loaded + if (null==getDatabase()) { + addActionError(Constants.ERROR_DATABASE_NOT_LOADED); + } + + if (hasErrors()) { + return ERROR; + } + else { + return SUCCESS; + } + } +} diff --git a/apps/mailreader/src/main/java/struts-bang.xml b/apps/mailreader/src/main/java/struts-bang.xml new file mode 100644 index 000000000..ecaef4f50 --- /dev/null +++ b/apps/mailreader/src/main/java/struts-bang.xml @@ -0,0 +1,44 @@ + + + +Loading ...
+ + diff --git a/apps/mailreader/src/main/webapp/pages/ChangePassword.jsp b/apps/mailreader/src/main/webapp/pages/ChangePassword.jsp new file mode 100644 index 000000000..667834d9f --- /dev/null +++ b/apps/mailreader/src/main/webapp/pages/ChangePassword.jsp @@ -0,0 +1,25 @@ +<%@ page contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="/struts-tags" prefix="s" %> + + + +
+
+ Please report this error to your system administrator + or appropriate technical support personnel. + Thank you for your cooperation. +
+ +
+
+
|
+ |
+
+ |
+
+ |
+
+ |
+
+ |
+
|---|---|---|---|---|
|
+ |
+
+ |
+
+ |
+
+ |
+
+
+ |
+
"
+ alt="
+A Walking Tour of the Struts 2 MailReader Application
+ ++ + 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. + + +
+ ++ + 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 + + Key Technologies Primer. + +
+ +
+ +Logging In
+ ++
+ +- + Welcome + +
++
+- web.xml and resources.properties
+ +- Welcome.do
+ +- Welcome Action
+ +- Global Results
+ +- ApplicationListener.java
+ +- Message Resources
+ +- Welcome Page
+ ++
+ +- + Logon +
++ +
+- Logon Page
+ +- Logon-validation.xml
+ +- Logon.java
+ +- MailreaderSupport.java
+ +- Logon Configuration
+ ++
+ +- + MainMenu +
++
+ +- + Registration page +
++
+- iterator
++
+- + Subscription + +
++
+- Subscription.java +
+
+ ++ 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. +
+ ++ 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. +
+ ++ For more about the MailReader, including alternate implementations and a + set of formal Use Cases, + please visit the + Struts University MailReader site. +
+ +
++++ JAAS - + Note that for compatibility and ease of deployment, the MailReader + uses "application-based" authorization. + However, use of the standard Java Authentication and Authorization + Service (JAAS) is recommended for most applications. + (See the + Key Technologies Primer for more about + authentication technologies.) +
+
+ ++ 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). +
+ +Welcome Page
+ ++ 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. +
+ +web.xml
+ ++ 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". +
+ +
+web.xml - The Web Application Deployment Descriptor
++<?xml version="1.0" encoding="ISO-8859-1"?> +<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" + "http://java.sun.com/dtd/web-app_2_3.dtd"> +<web-app> + + <display-name>Struts 2 MailReader</display-name> + + <filter> + <filter-name>struts2</filter-name> + <filter-class> + org.apache.struts2.dispatcher.FilterDispatcher + </filter-class> + </filter> + + <filter-mapping> + <filter-name>struts2</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>
+ ++ Among other things, + the web.xml specifies the "Welcome File List" for an application. + When a web address refers to a directory rather than an individual file, + the container consults the Welcome File List for the name of a page to + open by default. +
+ ++ However, most Struts applications do not refer to physical pages, + but to "virtual resources" called actions. + 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. +
+ +
+Best Practice:
+++"Link actions not pages."
+
+ ++ 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. +
+ ++ Sites can list zero or more "Welcome" pages in the web.xml. + Unless you are using Java 1.5, actions cannot be specified as a Welcome + page. + So, in the case of a Welcome page, + how do we follow the best practice of navigating through actions + rather than pages? +
+ ++ 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. +
+ +
+MailReader's index.html
++<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html><head> + <META HTTP-EQUIV="Refresh" CONTENT="0;URL=Welcome.do"> + </head> + <body> + <p>Loading ...</p> +</body></html>
+ ++ 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. +
+ +Welcome.do
+ ++ 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. +
+
+A simple "forward thru" action element
++<action name="Welcome"> + <result>/pages/Welcome.jsp</result> +</action>
+ ++ 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". +
+ ++ But if we peek at the configuration file for the MailReader, + we find a slightly more complicated XML element for the Welcome action. +
+ +
+The Welcome action element
++<action name="Welcome" class="mailreader2.Welcome"> + <result>/pages/Welcome.jsp</result> + <interceptor-ref name="guest"/> + </action>
+ ++ Here, the Welcome 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". +
+ +
+Key concept:
++++ 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. +
+
+ ++ The net effect is that all of the result details, + including the paths to server pages, + all can be declared once in the configuration. + Tightly coupled implementation details are not scattered all over + the application. +
+ +
+Key concept:
++++ 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. +
+
+ + ++ OK ... but why would a Welcome Action want to choose between "success" and + "error"? +
+ +Welcome Action
+ ++ The MailReader application retains a list of users along with their email + accounts. + The application stores this information in a database. + If the application can't connect to the database, the application can't do + its job. + So before displaying the Welcome page, the Welcome + class checks to see if the database is available. +
+ ++ 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. +
+ +
+The Welcome Action class
++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)) { + addActionError(Constants.ERROR_MESSAGES_NOT_LOADED); + } + + // Confirm database loaded + if (null==getDatabase()) { + addActionError(Constants.ERROR_DATABASE_NOT_LOADED); + } + + if (hasErrors()) { + return ERROR; + } + else { + return SUCCESS; + } + } +}
+ ++ 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. +
+ + +Global Results
+ ++ 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. +
+ +
+MailReader's global-result element
++<global-results> + <result name="error">/pages/Error.jsp</result> + <result name="invalid.token">/pages/Error.jsp</result> + <result name="login" type="redirect-action">Logon!input</result> +</global-results>
+ ++ Of course, if an individual action mapping defines its own "error" result type, + the local result would be used instead. +
+ +ApplicationListener.java +
+ ++ The database is exposed as an object stored in application scope. + The database object is based on an interface. + Different implementations of the database could be loaded without changing + the rest of the application. + But how is the database object loaded in the first place? +
+ ++ The database is created by a custom Listener that we configured in the "web.xml". +
+ +
+mailreader2.ApplicationListener
++<listener> + <listener-class> + mailreader2.ApplicationListener + </listener-class> +</listener>
+ ++ By default, our ApplicationListener loads a MemoryDatabase + implementation of the UserDatabase. + MemoryDatabase stores the database content as a XML document, + which is parsed and loaded as a set of nested hashtables. + The outer table is the list of user objects, each of which has its own + inner hashtable of subscriptions. + When you register, a user object is stored in this hashtable. + When you login, the user object is stored within the session context. +
+ ++ The database comes seeded with a sample user. + If you check the "database.xml" file under "/src/main", + you'll see the sample user described in XML. +
+ +
+The "seed" user element from the MailReader database.xml
++<user username="user" fromAddress="John.User@somewhere.com" + fullName="John Q. User" password="pass"> + <subscription host="mail.hotmail.com" autoConnect="false" + password="bar" type="pop3" username="user1234"> + </subscription> + <subscription host="mail.yahoo.com" autoConnect="false" password="foo" + type="imap" username="jquser"> + </subscription> +</user>
+ ++ The "seed" user element creates a registration record for "John Q. User", + with the subscription detail for his hotmail and yahoo accounts. +
+ +Message Resources +
+ ++ As mentioned, MailReader is an internationalized application. + The message resources for the application are loaded through a reference in the + "struts.properties" file. + Like the database contents, the "struts.properties" file is kept under + "/src/main/" in the source tree. +
+ +
+struts.properties
++struts.custom.i18n.resources = resources +struts.action.extension = do
+ ++ When we specify "resources" in the properties file, + we are telling the framework to scan the classpath + for a Resource Bundle named "resources.properties". + The bundle might be embedded in a JAR, or found in the "WEB-INF/classes" + folder, or anywhere else on the runtime classpath. + In the MailReader, we keep the original bundle in the + source tree under "src/main/". When the application is built, the + properties files are copied to "WEB-INF/classes", so + that they are on the Java classpath. +
+ +
+Message Resource entries used by the Welcome page
++index.heading=MailReader Application Options +index.logon=Log on to the MailReader Application +index.registration=Register with the MailReader Application +index.title=MailReader Demonstration Application +index.tour=A Walking Tour of the MailReader Demonstration Application
+ ++ 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. +
+ +Welcome Page
+ ++ After confirming that the necessary resources exist, the Welcome action + forwards to the Welcome page. +
+
+Welcome.jsp
++<%@ 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> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <title><s:text name="index.title"/></title> + <link href="<s:url value="/css/mailreader.css"/>" 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="Logon!input"/>"><s:text + name="index.logon"/></a></li> + </ul> + + <h3>Language Options</h3> + <ul> + <li><a href="<s:url action="Welcome?request_locale=en"/>">English</a></li> + <li><a href="<s:url action="Welcome?request_locale=ja"/>">Japanese</a></li> + <li><a href="<s:url action="Welcome?request_locale=ru"/>">Russian</a></li> + </ul> + + <hr /> + + <p><s:i18n name="alternate"> + <img src="<s:text name="struts.logo.path"/>" + alt="<s:text name="struts.logo.alt"/>"/> + </s:i18n></p> + + <p><a href="<s:url action="Tour" />"><s:text name="index.tour"/></a></p> + + </body> +</html>
+ ++ 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". +
+ ++ (We use the tag prefix "s:" in the Struts 2 MailReader application, + but you can use whatever prefix you like in your applications.) +
+ ++ The text 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. +
+ ++ The url 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. +
+ +
+Tip:
++++ Cookies - + If you turn cookies off in your browser, and then reload your browser + and this page, + you will see the links with the Java session id information attached. + (If you are using Internet Explorer and try this, + be sure you reset cookies for the appropriate security zone, + and that you disallow "per-session" cookies.) +
+
+ ++ The i18n 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. +
+ ++ The alternate bundle is stored next to the default bundle, + so that it ends up under "classes", which is on the application's class path. +
+ ++ In the span of a single request for the Welcome page, the framework has done + quite a bit already: +
+ ++
+ +- + Confirmed that required resources were loaded during initialization. +
+ +- + Written all the page headings and labels from internationalized + message resources. +
+ +- + Automatically URL-encoded paths as needed. +
++ 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 Logon link first. +
+ +Logon
+ ++ If you choose the Logon link, and all goes well, the Logon action forwards + control to the Logon page. +
+ +Logon Page
+ ++ The Logon page displays a form that accepts a username and password. + You can use the default username and password to logon + (user and pass), if + you like. 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. +
+ +
+Login.jsp
++<%@ 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="logon.title"/></title> + <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet" + type="text/css"/> + </head> + <body onLoad="self.focus();document.Logon.username.focus()"> + <s:actionerror/> + <s:form method="POST" validate="true"> + <s:textfield label="%{getText('username')}" name="username"/> + <s:password label="%{getText('password')}" name="password"/> + <s:submit value="%{getText('button.save')}"/> + <s:reset value="%{getText('button.reset')}"/> + <s:submit action="Logon!cancel" onclick="form.onsubmit=null" + value="%{getText('button.cancel')}"/> + </s:form> + <jsp:include page="Footer.jsp"/> + </body> +</html>
+ ++ We already saw some of the tags used by the Logon page on the Welcome page. + Let's focus on the new tags. +
+ ++ The first new tag on the Logon page is actionerrors. + 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. +
+ ++ The second new tag is form. + This tag renders a HTML form tag. + By default, the form will submit back to whatever action invoked the page. + 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. + You can use the method attribute to designate "GET" or "POST", + just like the HTML form tag. +
+ ++ 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. +
+ ++ 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. +
+ ++ The UI Tags support templates and themes so that a set of HTML tags can be + rendered from a single UI Tag. For example, the single tag +
+ ++ ++ <s:textfield label="%{getText('username')}" name="username"/> ++ generates a wad of HTML markup. +
+ +
++<tr> + <td class="tdLabel"> + <label for="Logon_username" class="label">Username:</label> + </td> + <td> + <input type="text" name="username" value="" id="Logon_username"/> + </td> +</tr>
+ ++ If for some reason you don't like the markup generated by a UI 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: +
+ +
++<#if (actionErrors?exists && actionErrors?size > 0)> + <ul> + <#list actionErrors as error> + <li><span class="errorMessage">${error}</span></li> + </#list> + </ul> +</#if>
+ ++ 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 "actionerror.ftl", + and place this one file somewhere on your classpath. +
+ +
++<#if (actionErrors?exists && actionErrors?size > 0)> + <table> + <#list actionErrors as error> + <tr><td><span class="errorMessage">${error}</span></td></tr> + </#list> + </table> +</#if>
+ ++ Under the covers, the framework uses + Freemarker + for its standard templating language. + FreeMarker is similar to + Velocity, + 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 UI Tags. +
+ ++ The password 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.) +
+ ++ Unsurprisingly, the submit and reset tags + render buttons of the corresponding types. +
+ ++ The second submit button is more interesting. +
+ ++ +<s:submit action="Logon!cancel" onclick="form.onsubmit=null" + value="%{getText('button.cancel')}"/> ++ Here we are creating the Cancel button for the form. + The button's attribute action="Logon!cancel" + tells the framework to submit to the Logon's "cancel" method + instead of the usual "execute" method. + The onclick="form.onsubmit=null" 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 cancel method. + (Other special aliases on the bypass list include "input" and "back".) +
+ +
+Tip:
++++ The UI tags have options and capabilities beyond what we have shown here. + For more see, the UI Tag documentation. +
+
+ ++ 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? +
+ ++ For the answers, we need to look at another flavor of configuration file: + the "validation" file. +
+ +Logon-validation.xml +
+ ++ While it is not hard to code data-entry validation into an Action class, + the framework provides an even easier way to validate input. +
+ ++ The validation framework is configured through another XML document, the + Logon-validation.xml. +
+ +
+Validation file for Logon Action
++<!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="username"> + <field-validator type="requiredstring"> + <message key="error.username.required"/> + </field-validator> + </field> + <field name="password"> + <field-validator type="requiredstring"> + <message key="error.password.required"/> + </field-validator> + </field> +</validators> +
+ ++ You may note that the DTD refers to "XWork". + + Open Symphony XWork + is a generic command-pattern framework that can be used outside of a + web environment. In practice, Struts 2 is a web-based extension of the + XWork framework. +
+ ++ The field elements correspond to the ActionForm properties. + The username and password 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. +
+ + + +Logon Action
+ ++ If validation passes, the framework invokes the "execute" method of the Logon Action. + The actual Logon Action is brief, since most of the functionality derives + from the base class, MailreaderSupport. +
+ +
+Logon.java
++package mailreader2; +import org.apache.struts.apps.mailreader.dao.User; +public final class Logon extends MailreaderSupport { +public String execute() throws ExpiredPasswordException { + User user = findUser(getUsername(), getPassword()); + if (user != null) { + setUser(user); + } + if (hasErrors()) { + return INPUT; + } + return SUCCESS; + } +}
+ ++ Logon 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. +
+ +MailreaderSupport.java
+ ++ Let's look at the relevant properties and methods from MailreaderSupport + and another base class, ActionSupport, namely + "getUsername", "getPassword", "findUser", "setUser", and "hasErrors". +
+ ++ The framework lets you define + JavaBean properties + 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. +
+ ++ The Username and Password properties are nothing fancy, + just standard JavaBean properties. +
+ +
+MailreaderSupport.getUsername() and getPassword()
++private String username = null; +public String getUsername() { + return this.username; +} +public void setUsername(String username) { + this.username = username; +} + +private String password = null; +public String getPassword() { + return this.password; +} +public void setPassword(String password) { + this.password = password; +}
+ ++ We use these properties to capture the client's credentials, + and pass them to the more interesting findUser method. +
+ +
+MailreaderSupport.findUser
++public User findUser(String username, String password) + throws ExpiredPasswordException { + User user = getDatabase().findUser(username); + if ((user != null) && !user.getPassword().equals(password)) { + user = null; + } + if (user == null) { + this.addFieldError("password", getText("error.password.mismatch")); + } + return user; +}
+ ++ The "findUser" method dips into the MailReader Data Access Object layer, + which is represented by the Database 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! +
+ +
+Best Practice:
+++"Strongly separate data access and business logic from the rest of the application." +
+
+ ++ When "findUser" returns, + the Logon Action looks to see if a valid (non-null) User object is returned. + A valid User is passed to the User property. + Although it is still a JavaBean property, + the User property is not implemented in quite the same way as Username and Password. +
+ +
+MailreaderSupport.setUser
++public User getUser() { + return (User) getSession().get(Constants.USER_KEY); +} +public void setUser(User user) { + getSession().put(Constants.USER_KEY, user); +}
+ ++ Instead of using a field to store the property value, + "setUser" passes it to a Session property. +
+ +
+MailreaderSupport.getSession() and setSession()
++private Map session; +public Map getSession() { + return session; + +public void setSession(Map value) { + session = value; +}
+ ++ 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. +
+ ++ But, when MailreaderSupport is running inside a web application, + how does it acquire a reference to the servlet session? +
+ ++ 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. +
+ ++ The magic that provides the Session property a runtime value is called "dependency injection". + The MailreaderSupport class implements a interface called SessionAware. + SessionAware is bundled with the framework, and it defines a setter for the Session property. +
+ ++
+ +public void setSession(Map session);++ Also bundled with the framework is an object called the ServletConfigInterceptor. + If the ServletConfigInterceptor sees that an Action implements the SessionAware interface, + it automatically set the session property. +
+ ++ +if (action instanceofSessionAware) { + ((SessionAware) action).setSession(context.getSession()); +}+ The framework uses these "Interceptor" classes to create a front controller + 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 + Filters, + 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.) +
+ ++ 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.xml configuration file. +
+ ++ Many Interceptors provide a utility or helper functions, like setting the session property. + Others, like the ValidationInterceptor, can change the workflow of an action. + Interceptors are key feature of the framework, + and we will see a few more on the tour. +
+ ++ If a valid User is not found, or the password doesn't match, + the "findUser" method invokes the addFieldError method to note the problem. + When "findUser" returns, the Logon Action checks for errors, + and then it returns either INPUT or SUCCESS. +
+ ++ 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. +
+ ++ 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. +
+ +
+Best Practice:
+++"Use a base class to define common functionality."
+
+ ++ But, what happens if Logon returns INPUT instead of SUCCESS. + How does the framework know what to do next? +
+ ++ To answer that question, + we need to turn back to the "struts.xml" file and look at how Logon is configured. +
+ + +Logon Configuration
+ ++ The Logon action element outlines how the Logon workflow operates, + including what to do when the Action returns "input", + or the default result name "success". +
+ +
+struts.xml Logon
++<action name="Logon" class="mailreader2.Logon"> + <result name="input">/pages/Logon.jsp</result> + <result name="cancel" type="redirect-action">Welcome</result> + <result type="redirect-action">MainMenu</result> + <result name="expired" type="chain">ChangePassword</result> + <exception-mapping + exception="org.apache.struts.apps.mailreader.dao.ExpiredPasswordException" + result="expired"/> + <interceptor-ref name="guest"/> +</action>
+ ++ In the Logon 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 + "Logon.jsp" page. +
+ ++ The second result element is named cancel. + If someone presses the cancel button on the Logon page, + the Action class will return "cancel", this result will be selected, + and the framework will issue a redirect to the Welcome action. +
+ ++ The third result has no name, + so it will be called if the default success token is returned. + So, if the Logon succeeds, + control will transfer to the MainMenu action. +
+ ++ 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. +
+ ++ Just in case any other Exceptions are thrown, + the MailReader application also defines a global handler. +
+ +
+struts.xml exception-mapping
++<global-exception-mappings> + <exception-mapping + result="error" + exception="java.lang.Exception"/> +</global-exception-mappings>
+ ++ 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. +
+ +
+Error.jsp
++<%@ 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> + <s:actionerror /> + <p> + <s:property value="%{exception.message}"/> + </p> + <hr /> + <h3>Technical Details</h3> + <p> + <s:property value="%{exceptionStack}"/> + </p> + <jsp:include page="Footer.jsp"/> + </body> +</html>
+ ++ The Error page uses property tags to expose + the Exception message and the Exception stack. +
+ ++ Finally, the Logon action specifies an InterceptorStack + named defaultStack. + If you've worked with Struts 2 or WebWork 2 before, that might seem strange, + since "defaultStack" is the factory default. +
+ ++ In the MailReader application, most of the actions are only available + to authenticated users. + The exceptions are the Welcome, Logon, and Register actions + which are available to everyone. + To authenticate clients, + the MailReader uses a custom Interceptor and a custom Interceptor stack. +
+ +
+mailreader2.AuthenticationInterceptor
++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 AuthenticationInterceptor implements Interceptor { + public void destroy () {} + public void init() {} + public String intercept(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 (isAuthenticated) { + return actionInvocation.invoke(); + } + else { + return Action.LOGIN; + } + } +}
+ ++ The AuthenticationInterceptor 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 Logon action. +
+ ++ The MailReader defines four custom Interceptor stacks: "user", "user-submit", + "guest", and "guest-submit". +
+ +
+struts.xml interceptors
++<interceptors> + <interceptor name="authentication" + class="mailreader2.AuthenticationInterceptor"/> + <interceptor-stack name="user" > + <interceptor-ref name="authentication" /> + <interceptor-ref name="defaultStack"/> + </interceptor-stack> + <interceptor-stack name="user-submit" > + <interceptor-ref name="token-session" /> + <interceptor-ref name="user"/> + </interceptor-stack> + <interceptor-stack name="guest" > + <interceptor-ref name="defaultStack"/> + </interceptor-stack> + <interceptor-stack name="guest-submit" > + <interceptor-ref name="token-session" /> + <interceptor-ref name="guest"/> + </interceptor-stack> +</interceptors> +<default-interceptor-ref name="user"/>
+ ++ The user stacks require that the client be authenticated. + In other words, that a User object is present in the session. + The actions using a guest stack can be accessed by any client. + The -submit versions of each can be used with actions + with forms, to guard against double submits. +
+ +Double Submits
+ ++ 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. +
+ ++ 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. +
+ ++ 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 +
+ ++ Because the default interceptor stack will now authenticate the client, + we need to specify the standard "defaultStack" for the three + "guest actions", Welcome, Logon, 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. +
+ +MainMenu
+ ++ On a successful logon, the Main Menu page displays. + If you logged in using the demo account, + the page title should be "Main Menu Options for John Q. User". + Below this legend should be two links: +
+ ++
+ +- + Edit your user registration profile +
+- + Log off MailReader Demonstration Application +
++ Let's review the source for the "MainMenu" action mapping, + and the "MainMenu.jsp". +
+ +
+Action mapping element for MainMenu
++ +<action name="MainMenu" class="mailreader2.MailreaderSupport"> + <result>/pages/MainMenu.jsp</result> + </action>MainMenu.jsp
++<%@ 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"/> <s:property + value="user.fullName"/></h3> + <ul> + <li><a href="<s:url action="Registration!input" />"> + <s:text name="mainMenu.registration"/> + </a> + </li> + <li><a href="<s:url action="Logoff" />"> + <s:text name="mainMenu.logoff"/> + </a> + </ul> + </body> +</html>
+ ++ The source for "MainMenu.jsp" also contains a new tag, + property, which we use to customize the page with the + "fullName" property of the authenticated user. +
+ ++ 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. +
+ ++ The customized MainMenu page offers two standard links. + One is to "Edit your user registration profile". + The other is to "Logoff the MailReader Demonstration Application". +
+ +Registration page +
+ ++ If you follow the "Edit your user registration profile" link from the Main + Menu page, + we will finally reach the heart of the MailReader application: the + Registration, or "Profile", page. + This page displays everything MailReader knows about you + (or at least your login), + while utilizing several interesting techniques. +
+ ++ To do double duty as the "Create" Registration page and the "Edit" + Registration page, + the "Registration.jsp" makes extensive use of the test tags, + to make it appears as though there are two distinct pages. +
+ +
+Registration.jsp - head element
++<head> + <s:if test="task=='Create'"> + <title><s:text name="registration.title.create"/></title> + </s:if> + <s:if test="task=='Edit'"> + <title><s:text name="registration.title.edit"/></title> + </s:if> + <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet" + type="text/css"/> +</head>
+ ++ 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. +
+ +
+Note:
++++ Presention Logic - + 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. +
+
+ ++ 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. +
+ +
+ ++<s:if test="task == 'Edit'"> + <div align="center"> + <h3><s:text name="heading.subscriptions"/></h3> + </div> + <!-- ... --> + </s:if> +<jsp:include page="Footer.jsp"/> +</body></html>
+ ++ Otherwise, the page contains just the top portion -- + a data-entry form for managing the user's registration. +
+ +iterator
+ ++ 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 iterator + tag to display the User's Subscriptions. +
+ ++ The subscriptions are stored in a hashtable object, which is in turn + stored in the user object. + So to display each subscription, we have to reach into the user object, + and loop through the members of the subscription collection. + Using the iterator tag, you can code it the way it sounds. +
+ +
+Using iterator to list the Subscriptions
++<s:iterator value="user.subscriptions"> + <tr> + <td align="left"> + <s:property value="host"/> + </td> + <td align="left"> + <s:property value="username"/> + </td> + <td align="center"> + <s:property value="type"/> + </td> + <td align="center"> + <s:property value="autoConnect"/> + </td> + <td align="center"> + <a href="<s:url action="Subscription!delete"><s:param name="host" value="host"/></s:url>"> + <s:text name="registration.deleteSubscription"/> + </a> + <a href="<s:url action="Subscription!edit"><s:param name="host" value="host"/></s:url>"> + <s:text name="registration.editSubscription"/> + </a> + </td> + </tr> +</s:iterator>
+ ++ When the iterator renders, it generates a list of Subscriptions for the current User. +
+ +
+ +++ +Current Subscriptions
++
+ Add + ++ ++ Host Name + ++ User Name + + ++ Server Type + ++ Auto + ++ Action + ++ ++ mail.hotmail.com + ++ user1234 + ++ pop3 + + ++ false + ++ + Delete + + + + Edit + + ++ ++ mail.yahoo.com + ++ jquser + ++ imap + ++ false + ++ + Delete + + + + Edit + + +
+ ++ Now look back at the code used to generate this block. +
++ Notice anything nifty? +
++ How about that the markup between the iterator tag is + actually simpler than the markup that we would use to render one row of the table? +
++ 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. +
++ Nice trick! +
+ ++ The secret to this magic is the value stack. + 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. +
+ ++ 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. +
+ ++ 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.) +
+ ++ 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. +
+ ++ 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. +
+ ++ 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. +
+ ++ 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. +
+ ++ The Subscription list uses another new tag: the param 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. +
+ ++ 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. +
+ ++ +pager.legend = Displaying {current} of {count} items matching {criteria}. +... +<s:text name="pager.legend"> + <s:param name="current" value="42" /> + <s:param name="count" value="314" /> + <s:param name="criteria" value="Life, the Universe, and Everything" /> +</s:text>+ In the case of an "url" tag, + we can use "param" to create the query string. + A statement like this: +
+ ++ ++ <s:url action="Subscription!edit"><s:param name="host" value="host"/></s:url>"> ++ can render a hyperlink like this: +
+ ++ + + + ++ <a href="/struts2-mailreader/Subscription!edit.do?host=mail.yahoo.com">Edit</a> ++ If a hyperlink needs more parameters, + you can use "param" to add as many parameters as needed. +
+ ++ Subscription +
+ ++ If we follow one of the "Edit" subscription links on the Registration page, + we come to the Subscriptions page, + which displays the details of our description in a data-entry form. + Let's have a look a the Subscription configuration in "struts.xml" + and follow the bouncing ball from page to action to page. +
+ +
+struts.xml Subscription element
++<action name="Subscription" class="mailreader2.Subscription"> + <result name="input">/pages/Subscription.jsp</result> + <result type="redirect-action">Registration!input</result> +</action>
+ ++ The Edit link specified the Subscription action, + but also includes the qualifier !edit. + The ! idiom tells the framework to invoke the + "edit" method of the Subscription action, + instead of the default "execute" method + The "alternate" execute methods are called alias methods. +
+ +
+Subscription edit alias
++public String edit() { + setTask(Constants.EDIT);> + return find(); +} + +public String find() { + org.apache.struts.apps.mailreader.dao.Subscription + sub = findSubscription(); + if (sub == null) { + return ERROR; + } + setSubscription(sub); + return INPUT; +}
+ ++ 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. +
+ ++ 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. +
+ ++ The business logic for the "edit" alias is a simple wrapper + around the MailReader DAO classes. +
+ +
+MailreaderSupport findSubscription()
++public Subscription findSubscription() { + return findSubscription(getHost()); +} + +public Subscription findSubscription(String host) { + Subscription subscription; + subscription = getUser().findSubscription(host); + return subscription; +}
+ ++ 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 "struts.xml" + will trap the exception for us. +
+ ++ Likewise, the AuthentificationInterceptor will ensure that only clients + with a valid User object can try to edit a Subscription. + If the session expired, or someone bookmarked the page, + the client will be redirected to the Logon page automatically. +
+ ++ As a final layer of defense, we also configured a validator for Subscription, + to ensure that we are passed a Host parameter. +
+ +
+Subscription-validation.xml
++<!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="host"> + <field-validator type="requiredstring"> + <message key="error.host.required"/> + </field-validator> + </field> +</validators>
+ ++ By keeping routine sety precautions out of the Action class, + the all-important Actions becomes smaller and easier to maintain. +
+ ++ After setting the relevent Subscription object to the Subscription property, + the framework transfers control to the (you guessed it) Subscription page. +
+ +
+Subscription.jsp
++<%@ 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 method="POST" action="SubscriptionSave" validate="false"> + <s:token /> + <s:hidden name="task"/> + <s:label label="%{getText('username')}" name="user.username"/> + + <s:if test="task == 'Create'"> + <s:textfield label="%{getText('mailHostname')}" name="host"/> + </s:if> + <s:else> + <s:label label="%{getText('mailHostname')}" name="host"/> + <s:hidden name="host"/> + </s:else> + + <s:if test="task == 'Delete'"> + <s:label label="%{getText('mailUsername')}" + name="subscription.username"/> + <s:label label="%{getText('mailPassword')}" + name="subscription.password"/> + <s:label label="%{getText('mailServerType')}" + name="subscription.type"/> + <s:label label="%{getText('autoConnect')}" + name="subscription.autoConnect"/> + <s:submit value="%{getText('button.confirm')}"/> + </s:if> + <s:else> + <s:textfield label="%{getText('mailUsername')}" + name="subscription.username"/> + <s:textfield label="%{getText('mailPassword')}" + name="subscription.password"/> + <s:select label="%{getText('mailServerType')}" + name="subscription.type" list="types"/> + <s:checkbox label="%{getText('autoConnect')}" + name="subscription.autoConnect"/> + <s:submit value="%{getText('button.save')}"/> + <s:reset value="%{getText('button.reset')}"/> + </s:else> + + <s:submit action="Registration!input" + value="%{getText('button.cancel')}" + onclick="form.onsubmit=null"/> + </s:form> + + <jsp:include page="Footer.jsp"/> + </body> +</html>
+ ++ As before, we'll discuss the tags and attributes that are new to this page: + "token", "hidden", "label", "select", and "checkbox". +
+ ++ When we looked at the form tag for the Logon page, + it did not specify a target for the submit. + Instead, it just posted back to the Logon action. + In this form tag, we are specifying a different action, + SubscriptionSave + to be the target of the submit, +
+ ++ The main reason we use another action is so that we can use a different set of validations. + When we retrieve the Subscription for editing, all we need is the Host property. + When we save the Subscription, we want to validate additional properties. + Since the validation files are coupled to the classes, + we created a new Action class for saving a Subscription. +
+ +
+Subscription-validation.xml
++<!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="host"> + <field-validator type="requiredstring"> + <message key="error.host.required"/> + </field-validator> + </field> +</validators>
+ ++ The validators follow the same type of inheritance path as the classes. + SubscriptionSave extends Subscription, + so when SubscriptionSave is validated, + the Host property specified by "Subscription-validation.xml" will also be required. +
+ ++ The token 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. +
+ ++ The hidden tag embeds the Task property into the form. + When the form is submitted, + the SubscriptionSave action wil use the Task property to decide + whether to insert or update the form. +
+ ++ The label 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. +
+ ++ Saving the best for last, the Subscription utilizes two more interesting + tags, "select" and "checkbox". +
+ ++ Unsurprisingly, the select tag renders a select control, + but the tag does so without requiring a lot of markup or redtape. +
+ ++ +<s:select label="%{getText('mailServerType')}" + name="subscription.type" list="types" /> ++ 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". +
+ +
+Subscription-validation.xml
++public class Subscription extends MailreaderSupport + implements Preparable { + + private Map types = null; + public Map getTypes() { + return types; + } + + public void prepare() { + Map m = new LinkedHashMap(); + m.put("imap", "IMAP Protocol"); + m.put("pop3", "POP3 Protocol"); + types = m; + setHost(getSubscriptionHost()); + } + + // ...
+ ++ The default Interceptor stack includes the PrepareInterceptor, + which observes the Preparable interface. +
+ +
+PrepareInterceptor
++ +public class PrepareInterceptor extends AroundInterceptor { + + protected void after(ActionInvocation dispatcher, String result) throws Exception { + } + + protected void before(ActionInvocation invocation) throws Exception { + Object action = invocation.getAction(); + if (action instanceof Preparable) { + ((Preparable) action).prepare(); + } + } +}+ 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. +
+ + ++ SubscriptionAction.java +
+ ++ 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, + and checkboxes need to be handled differently that other controls. +
+ ++ The checkbox starts out as a simple enough control. +
+ ++ +<s:checkbox label="%{getText('autoConnect')}" + name="subscription.autoConnect"/>+ 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 anything. + 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. +
+ +
+Tip:
++++ Checkboxes - + The HTML checkbox is a tricky control. + The problem is that, according to the W3C specification, a value is + only guaranteed to be sent + if the control is checked. + If the control is not checked, then the control may be omitted from + the request, as if it was on the page. + This can cause a problem with session-scope checkboxes. + Once you set the checkbox to true, the control can't set it to false + again, + because if you uncheck the box, nothing is sent, and so the control + stays checked. +
+
+ ++ The simplest solution is to employ our old friend Preparable again. + In the "prepare" method for SubscriptionSave, + we can set the property represented by the checkbox to false. + If the control is not submitted, then the property remains false. + If the control is submitted, then the property is set to true. +
+ +
+SubscriptionSave
++public final class SubscriptionSave extends Subscription { + + public void prepare() { + super.prepare(); + // checkbox workaround + getSubscription().setAutoConnect(false); + } + + public String execute() throws Exception { + return save(); + } +}
+ + ++ If we press the SAVE button, + the form will be submitted to the SubscriptionSave action. + If the validation succeeds, as we've seen, + SubscriptionSave will invoke the Subscription.save method. +
+ +
+Subscription save method
++public String save() throws Exception { + + if (Constants.DELETE.equals(getTask())) { + removeSubscription(); + } + + if (Constants.CREATE.equals(getTask())) { + copySubscription(getHost()); + } + + saveUser(); + return SUCCESS; +}
+ ++ The save method uses the Task property to handle + the special cases of deleting and creating, + and then updates the state of the User object. +
+ ++ The removeSubscription method calls the DAO facade, + and then updates the application state. +
+ +
+removeSubscription
++public void removeSubscription() throws Exception { + getUser().removeSubscription(getSubscription()); + getSession().remove(Constants.SUBSCRIPTION_KEY); +}
+ ++ The copySubscription 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. +
+ ++ 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. +
+ ++ Another workaround is to declare only the immutable properties on the Action, + and then use what we can from the domain object. +
+ ++ 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. +
+ ++ 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. +
+ +
+copySubscription
++ +public void copySubscription(String host) { + Subscription input = getSubscription(); + Subscription sub = createSubscription(host); + if (null != sub) { + BeanUtils.setValues(sub, input, null); + setSubscription(sub); + setHost(sub.getHost()); + } +}+ 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. +
+ +Summary
++ 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 + available online + and in the distribution. +
+ ++ Enjoy! +
+ + + diff --git a/apps/mailreader/src/main/webapp/struts-power.gif b/apps/mailreader/src/main/webapp/struts-power.gif new file mode 100644 index 000000000..5f4e9d426 Binary files /dev/null and b/apps/mailreader/src/main/webapp/struts-power.gif differ