diff --git a/apps/mailreader/pom.xml b/apps/mailreader/pom.xml
deleted file mode 100644
index cf23ee641..000000000
--- a/apps/mailreader/pom.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
ServletContextListener that initializes and finalizes the
- * persistent storage of User and Support 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:
- * - *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 deleted file mode 100644 index 627c456bc..000000000 --- a/apps/mailreader/src/main/java/mailreader2/AuthenticationInterceptor.java +++ /dev/null @@ -1,33 +0,0 @@ -package mailreader2; - -import com.opensymphony.xwork2.Action; -import com.opensymphony.xwork2.ActionInvocation; -import com.opensymphony.xwork2.interceptor.Interceptor; -import org.apache.struts.apps.mailreader.dao.User; - -import java.util.Map; - -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/ChangePassword.java b/apps/mailreader/src/main/java/mailreader2/ChangePassword.java deleted file mode 100644 index 9bd958b1d..000000000 --- a/apps/mailreader/src/main/java/mailreader2/ChangePassword.java +++ /dev/null @@ -1,10 +0,0 @@ -package mailreader2; - -/** - *- * When implemented, allow guest to change password, - * given correct credentials. - *
- */ -public class ChangePassword extends MailreaderSupport { -} diff --git a/apps/mailreader/src/main/java/mailreader2/Constants.java b/apps/mailreader/src/main/java/mailreader2/Constants.java deleted file mode 100644 index 45afd4860..000000000 --- a/apps/mailreader/src/main/java/mailreader2/Constants.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * $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 { - - // -- Statements -- - - public static final Integer DB_FALSE = 0; - public static final Integer DB_TRUE = 1; - - public static final String LOCALE_LIST = "LOCALE_LIST"; - public static final String REGISTRATION_INSERT_ASSERT = "REGISTRATION_INSERT_ASSERT"; - public static final String REGISTRATION_INSERT = "REGISTRATION_INSERT"; - public static final String REGISTRATION_PASSWORD = "REGISTRATION_PASSWORD"; - public static final String REGISTRATION_FULLNAME = "REGISTRATION_FULLNAME"; - public static final String REGISTRATION_EDIT = "REGISTRATION_EDIT"; - public static final String REGISTRATION_UPDATE = "REGISTRATION_UPDATE"; - public static final String SUBSCRIPTION_INSERT_ASSERT = "SUBSCRIPTION_INSERT_ASSERT"; - public static final String SUBSCRIPTION_INSERT = "SUBSCRIPTION_INSERT"; - public static final String SUBSCRIPTION_LIST = "SUBSCRIPTION_LIST"; - public static final String SUBSCRIPTION_EDIT = "SUBSCRIPTION_EDIT"; - public static final String SUBSCRIPTION_UPDATE = "SUBSCRIPTION_UPDATE"; - - // -- 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 Support 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"; - - /** - * - * A static message in case message resource is not loaded. - * - */ - public static final String ERROR_INVALID_WORKFLOW = - "ERROR: Action is being executed out of sequence!"; - - // ---- 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 deleted file mode 100644 index 4d8839307..000000000 --- a/apps/mailreader/src/main/java/mailreader2/Logoff.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * $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/MailreaderSupport.java b/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.java deleted file mode 100644 index 1f4b64829..000000000 --- a/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.java +++ /dev/null @@ -1,558 +0,0 @@ -/* - * $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 com.opensymphony.xwork2.ActionSupport; -import com.opensymphony.xwork2.ModelDriven; -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 org.apache.struts2.interceptor.ApplicationAware; -import org.apache.struts2.interceptor.SessionAware; - -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 Support 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 - * Support.
- */ - private String host; - - /** - *Provide tSubscription host.
- * - * @return host property - */ - public String getHost() { - return host; - } - - /** - *Store new Support 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); - } - } - - // ---- Support property ---- - - /** - *Obtain the cached Support object, if any.
- * - * @return Cached Support object or null - */ - public Subscription getSubscription() { - return (Subscription) getSession().get(Constants.SUBSCRIPTION_KEY); - } - - /** - *Store new User Support.
- * - * @param subscription - */ - public void setSubscription(Subscription subscription) { - getSession().put(Constants.SUBSCRIPTION_KEY, subscription); - } - - /** - *Obtain User Support 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 Support or null - */ - public Subscription findSubscription(String host) { - Subscription subscription; - subscription = getUser().findSubscription(host); - return subscription; - } - - /** - *Obtain uSER Support 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 Support or null if not found - */ - public Subscription findSubscription() { - return findSubscription(getHost()); - } - - /** - *
Provide a "temporary" User Support 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 Support object for the given host, or null if - * the host is not unique.
- * - * @param host - * @return New User Support 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 Support from the current Support object, - * making the new Support the current Support.
- * - *Usually, the "current" Support 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 Support object from the database.
- */ - public void removeSubscription() { - getUser().removeSubscription(getSubscription()); - getSession().remove(Constants.SUBSCRIPTION_KEY); - } - - /** - *Provide MailServer Host for current User Support.
- * - * @return MailServer Host for current User Support - */ - 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 deleted file mode 100644 index df33d47cc..000000000 --- a/apps/mailreader/src/main/java/mailreader2/MailreaderSupport.properties +++ /dev/null @@ -1,93 +0,0 @@ -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:
- * Provide access to MailreaderSupport properties. - *
- */ -public class MainMenu extends MailreaderSupport { -} diff --git a/apps/mailreader/src/main/java/mailreader2/Welcome-action.xml b/apps/mailreader/src/main/java/mailreader2/Welcome-action.xml deleted file mode 100644 index 8577d073c..000000000 --- a/apps/mailreader/src/main/java/mailreader2/Welcome-action.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -Stub class for login/Input.
- */ -public class Input extends MailreaderSupport { -} diff --git a/apps/mailreader/src/main/java/mailreader2/logon/Retrieve-validation.xml b/apps/mailreader/src/main/java/mailreader2/logon/Retrieve-validation.xml deleted file mode 100644 index 4a04c7629..000000000 --- a/apps/mailreader/src/main/java/mailreader2/logon/Retrieve-validation.xml +++ /dev/null @@ -1,14 +0,0 @@ - - -Validate a user logon.
- */ -public final class Retrieve 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/registration/Create-validation.xml b/apps/mailreader/src/main/java/mailreader2/registration/Create-validation.xml deleted file mode 100644 index ccf277b4a..000000000 --- a/apps/mailreader/src/main/java/mailreader2/registration/Create-validation.xml +++ /dev/null @@ -1,28 +0,0 @@ - - -Insert new user, providing extra validation.
- * - *(On an update, the password change is optional.)
- */ -public final class Create extends Update { - - public String execute() - throws Exception { - - // trust but verify - boolean creating = Constants.CREATE.equals(getTask()); - creating = creating && isCreating(); - if (!creating) { - addActionError("registration/Create: "); - addActionError(Constants.ERROR_INVALID_WORKFLOW); - return ERROR; - } - - User user = findUser(getUsername(), getPassword()); - boolean haveUser = (user != null); - - if (haveUser) { - addActionError(getText("error.username.unique")); - return INPUT; - } - - copyUser(getUsername(), getPassword()); - - return SUCCESS; - } -} diff --git a/apps/mailreader/src/main/java/mailreader2/registration/Input.java b/apps/mailreader/src/main/java/mailreader2/registration/Input.java deleted file mode 100644 index a034366cf..000000000 --- a/apps/mailreader/src/main/java/mailreader2/registration/Input.java +++ /dev/null @@ -1,31 +0,0 @@ -package mailreader2.registration; - -import mailreader2.Constants; - -/** - *Retrieve User object to edit or null if User does not exist.
- */ -public class Input extends Support { - - /** - *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 execute() throws Exception { - - if (isCreating()) { - createInputUser(); - setTask(Constants.CREATE); - } else { - setTask(Constants.EDIT); - setUsername(getUser().getUsername()); - setPassword(getUser().getPassword()); - setPassword2(getUser().getPassword()); - } - - return SUCCESS; - } - -} diff --git a/apps/mailreader/src/main/java/mailreader2/registration/Support.java b/apps/mailreader/src/main/java/mailreader2/registration/Support.java deleted file mode 100644 index 0316314e1..000000000 --- a/apps/mailreader/src/main/java/mailreader2/registration/Support.java +++ /dev/null @@ -1,20 +0,0 @@ -package mailreader2.registration; - -import mailreader2.MailreaderSupport; -import org.apache.struts.apps.mailreader.dao.User; - -/** - *Base class to store shared methods.
- */ -public class Support extends MailreaderSupport { - - /** - *Double check that there is not a valid User logon.
- * - * @return True if there is not a valid User logon - */ - protected boolean isCreating() { - User user = getUser(); - return (null == user) || (null == user.getDatabase()); - } -} diff --git a/apps/mailreader/src/main/java/mailreader2/registration/Update-validation.xml b/apps/mailreader/src/main/java/mailreader2/registration/Update-validation.xml deleted file mode 100644 index 44d66bc19..000000000 --- a/apps/mailreader/src/main/java/mailreader2/registration/Update-validation.xml +++ /dev/null @@ -1,32 +0,0 @@ - - -Update Registration object to the persistent store.
- */ -public class Update extends Support { - - private String fixNull(String value) { - if (value == null) return null; - String buffer = value.trim(); - if (buffer.length() == 0) return null; - return buffer; - } - - public String execute() - throws Exception { - - boolean creating = Constants.CREATE.equals(getTask()); - creating = creating && isCreating(); // trust but verify - if (creating) { - addActionError("registration/Update: "); - addActionError(Constants.ERROR_INVALID_WORKFLOW); - return ERROR; - } - // FIXME: Any way to call the Update validators from here? - String newPassword = fixNull(getPassword()); - String confirmPassword = fixNull(getPassword2()); - if (newPassword != null) { - boolean matches = ((confirmPassword != null) - && (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/Delete.java b/apps/mailreader/src/main/java/mailreader2/subscription/Delete.java deleted file mode 100644 index 893916f01..000000000 --- a/apps/mailreader/src/main/java/mailreader2/subscription/Delete.java +++ /dev/null @@ -1,15 +0,0 @@ -package mailreader2.subscription; - -import mailreader2.Constants; - -/** - *Prepare to present a confirmation page before removing - * subscription.
- */ -public class Delete extends Edit { - - public String execute() throws Exception { - setTask(Constants.DELETE); - return find(); - } -} diff --git a/apps/mailreader/src/main/java/mailreader2/subscription/Edit-validation.xml b/apps/mailreader/src/main/java/mailreader2/subscription/Edit-validation.xml deleted file mode 100644 index df903c25b..000000000 --- a/apps/mailreader/src/main/java/mailreader2/subscription/Edit-validation.xml +++ /dev/null @@ -1,11 +0,0 @@ - - -Prepare to edit User Support.
- */ -public class Edit extends Support { - - public String execute() throws Exception { - setTask(Constants.EDIT); - return find(); - } - - -} diff --git a/apps/mailreader/src/main/java/mailreader2/subscription/Input.java b/apps/mailreader/src/main/java/mailreader2/subscription/Input.java deleted file mode 100644 index 2053dd617..000000000 --- a/apps/mailreader/src/main/java/mailreader2/subscription/Input.java +++ /dev/null @@ -1,17 +0,0 @@ -package mailreader2.subscription; - -import mailreader2.Constants; - -/** - *Setup a temporary User Support object to capture input - * values.
- */ -public class Input extends Support { - - public String execute() { - createInputSubscription(); - setTask(Constants.CREATE); - return SUCCESS; - } - -} diff --git a/apps/mailreader/src/main/java/mailreader2/subscription/Support.java b/apps/mailreader/src/main/java/mailreader2/subscription/Support.java deleted file mode 100644 index 1b880e99a..000000000 --- a/apps/mailreader/src/main/java/mailreader2/subscription/Support.java +++ /dev/null @@ -1,69 +0,0 @@ -package mailreader2.subscription; - -import com.opensymphony.xwork2.Preparable; -import mailreader2.MailreaderSupport; -import mailreader2.Constants; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - *Provide an Edit method for retrieving an existing subscription, and a - * Update method for updating or inserting a subscription.
- */ -public class Support 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 Support (if any).
- */ - public void prepare() { - - Map m = new LinkedHashMap(); - m.put("imap", "IMAP Protocol"); - m.put("pop3", "POP3 Protocol"); - types = m; - - setHost(getSubscriptionHost()); - - } - - /** - *Load User Support 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 Support is not found - */ - public String find() { - - org.apache.struts.apps.mailreader.dao.Subscription - sub = findSubscription(); - - if (sub == null) { - addActionError("find (Subscription): "); - addActionError(Constants.ERROR_INVALID_WORKFLOW); - return ERROR; - } - - setSubscription(sub); - - return SUCCESS; - } -} diff --git a/apps/mailreader/src/main/java/mailreader2/subscription/Update-validation.xml b/apps/mailreader/src/main/java/mailreader2/subscription/Update-validation.xml deleted file mode 100644 index 9f2f6d793..000000000 --- a/apps/mailreader/src/main/java/mailreader2/subscription/Update-validation.xml +++ /dev/null @@ -1,23 +0,0 @@ - - -Workaround class. Submitting to an alias doesn't seem to work.
- */ -public final class Update extends Edit { - - public void prepare() { - super.prepare(); - // checkbox workaround - getSubscription().setAutoConnect(false); - } - - /** - *Examine the Task property and DELETE, CREATE, or save the User - * Support, as appropriate.
- * - * @return SUCCESS - * @throws Exception on a database error - */ - public String execute() 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/struts.properties b/apps/mailreader/src/main/java/struts.properties deleted file mode 100644 index 8e355e3f3..000000000 --- a/apps/mailreader/src/main/java/struts.properties +++ /dev/null @@ -1,3 +0,0 @@ -struts.objectFactory = spring -struts.devMode = true -struts.enable.DynamicMethodInvocation = false diff --git a/apps/mailreader/src/main/java/struts.xml b/apps/mailreader/src/main/java/struts.xml deleted file mode 100644 index 4e7a8e15c..000000000 --- a/apps/mailreader/src/main/java/struts.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - -Loading ...
- - diff --git a/apps/mailreader/src/main/webapp/pages/ChangePassword.jsp b/apps/mailreader/src/main/webapp/pages/ChangePassword.jsp deleted file mode 100644 index 37d85bec1..000000000 --- a/apps/mailreader/src/main/webapp/pages/ChangePassword.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ page contentType="text/html; charset=UTF-8" %> -<%@ taglib prefix="s" uri="/struts-tags" %> - - - -
-
- 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.registration.Retrieve"> - <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.SubscriptionSupport"> - <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 deleted file mode 100644 index 5f4e9d426..000000000 Binary files a/apps/mailreader/src/main/webapp/struts-power.gif and /dev/null differ diff --git a/apps/mailreader/src/test/java/mailreader2/ConfigTest.java b/apps/mailreader/src/test/java/mailreader2/ConfigTest.java deleted file mode 100644 index abe1127f7..000000000 --- a/apps/mailreader/src/test/java/mailreader2/ConfigTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package mailreader2; - -import com.opensymphony.xwork2.XWorkTestCase; -import com.opensymphony.xwork2.ActionSupport; -import com.opensymphony.xwork2.config.RuntimeConfiguration; -import com.opensymphony.xwork2.config.entities.ActionConfig; -import com.opensymphony.xwork2.config.entities.ResultConfig; -import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider; - -import java.util.Map; - -public class ConfigTest extends XWorkTestCase { - - protected void setUp() throws Exception { - super.setUp(); - XmlConfigurationProvider c = new XmlConfigurationProvider("struts.xml"); - configurationManager.addConfigurationProvider(c); - configurationManager.reload(); - } - - private ActionConfig assertClass(String action_name, String class_name) { - RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration(); - ActionConfig config = configuration.getActionConfig("", action_name); - assertNotNull(config); - assertTrue("Wrong class name: [" + config.getClassName() + "]", - class_name.equals(config.getClassName())); - return config; - } - - private void assertResult(ActionConfig config, String result_name, String result_value) { - Map results = config.getResults(); - ResultConfig result = (ResultConfig) results.get(result_name); - Map params = result.getParams(); - String value = (String) params.get("actionName"); - if (value==null) - value = (String) params.get("location"); - assertTrue("Wrong result value: [" + value + "]", - result_value.equals(value)); - } - - public void testSubscriptionSave() throws Exception { - ActionConfig config = assertClass("subscription/Update", "mailreader2.subscription.Update"); - assertResult(config, ActionSupport.SUCCESS, "registration/+Input"); - assertResult(config, ActionSupport.INPUT, "/pages/subscription.jsp"); - } - - // */!* - public void testPrepareWildcard() throws Exception { - ActionConfig config = assertClass("logon/+Input", "mailreader2.logon.Input"); - assertResult(config, ActionSupport.SUCCESS, "/pages/logon.jsp"); - } - - // */* - public void testExecuteWildcard() throws Exception { - ActionConfig config = assertClass("registration/Update", "mailreader2.registration.Update"); - assertResult(config, ActionSupport.INPUT, "/pages/registration.jsp"); - ActionConfig config2 = assertClass("logon/Retrieve", "mailreader2.logon.Retrieve"); - assertResult(config2, ActionSupport.INPUT, "/pages/logon.jsp"); - } - - // single wildcard - public void testDisplayWildcard() throws Exception { - assertClass("Welcome", "mailreader2.Welcome"); - } - -}