You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2019/12/13 10:10:34 UTC
[struts-examples] 01/01: Re-implements mailreader example to be
independent from Struts 1
This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch new-mailreader
in repository https://gitbox.apache.org/repos/asf/struts-examples.git
commit 8bc7a0be4aba50ec83811b97057e7f5d53b6e481
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Fri Dec 13 11:00:33 2019 +0100
Re-implements mailreader example to be independent from Struts 1
---
mailreader2/README.txt | 23 +
mailreader2/pom.xml | 72 +
.../examples/mailreader2/ApplicationListener.java | 203 ++
.../mailreader2/AuthenticationInterceptor.java | 47 +
.../struts/examples/mailreader2/Constants.java | 126 +
.../struts/examples/mailreader2/LoginAction.java | 41 +
.../struts/examples/mailreader2/LogoutAction.java | 32 +
.../examples/mailreader2/MailreaderSupport.java | 578 +++++
.../examples/mailreader2/RegistrationAction.java | 119 +
.../examples/mailreader2/SubscriptionAction.java | 135 ++
.../struts/examples/mailreader2/WelcomeAction.java | 47 +
.../mailreader2/dao/ExpiredPasswordException.java | 37 +
.../examples/mailreader2/dao/Subscription.java | 90 +
.../struts/examples/mailreader2/dao/User.java | 126 +
.../examples/mailreader2/dao/UserDatabase.java | 96 +
.../mailreader2/dao/impl/AbstractSubscription.java | 179 ++
.../mailreader2/dao/impl/AbstractUser.java | 236 ++
.../dao/impl/memory/MemorySubscription.java | 28 +
.../mailreader2/dao/impl/memory/MemoryUser.java | 71 +
.../dao/impl/memory/MemoryUserDatabase.java | 317 +++
mailreader2/src/main/resources/LICENSE.txt | 174 ++
mailreader2/src/main/resources/NOTICE.txt | 5 +
.../src/main/resources/alternate.properties | 3 +
.../src/main/resources/alternate_ja.properties | 1 +
mailreader2/src/main/resources/log4j2.xml | 16 +
.../src/main/resources/mailreader-default.xml | 47 +
.../src/main/resources/mailreader-support.xml | 63 +
.../examples/mailreader2/Login-validation.xml | 14 +
.../mailreader2/MailreaderSupport.properties | 97 +
.../mailreader2/MailreaderSupport_ja.properties | 89 +
.../mailreader2/MailreaderSupport_ru.properties | 89 +
.../Registration-Registration_save-validation.xml | 28 +
.../mailreader2/Registration-validation.xml | 32 +
.../Subscription-Subscription_save-validation.xml | 23 +
.../mailreader2/Subscription-validation.xml | 11 +
mailreader2/src/main/resources/struts.xml | 16 +
mailreader2/src/main/resources/velocity.properties | 1 +
mailreader2/src/main/webapp/META-INF/context.xml | 3 +
mailreader2/src/main/webapp/WEB-INF/database.xml | 9 +
.../src/main/webapp/WEB-INF/jsp/ChangePassword.jsp | 25 +
mailreader2/src/main/webapp/WEB-INF/jsp/Error.jsp | 40 +
mailreader2/src/main/webapp/WEB-INF/jsp/Footer.jsp | 6 +
mailreader2/src/main/webapp/WEB-INF/jsp/Login.jsp | 30 +
.../src/main/webapp/WEB-INF/jsp/MainMenu.jsp | 25 +
.../src/main/webapp/WEB-INF/jsp/Registration.jsp | 115 +
.../src/main/webapp/WEB-INF/jsp/Subscription.jsp | 60 +
.../src/main/webapp/WEB-INF/jsp/Welcome.jsp | 55 +
mailreader2/src/main/webapp/WEB-INF/web.xml | 45 +
mailreader2/src/main/webapp/css/mailreader.css | 46 +
mailreader2/src/main/webapp/index.html | 10 +
mailreader2/src/main/webapp/struts-power.gif | Bin 0 -> 1798 bytes
mailreader2/src/main/webapp/tour.html | 2470 ++++++++++++++++++++
pom.xml | 1 +
53 files changed, 6252 insertions(+)
diff --git a/mailreader2/README.txt b/mailreader2/README.txt
new file mode 100644
index 0000000..1ce884b
--- /dev/null
+++ b/mailreader2/README.txt
@@ -0,0 +1,23 @@
+README.txt - mailreader
+
+The MailReader demonstrates a localized application with a master/child
+CRUD workflow.
+
+This rendition also demonstrates using wildcards to "normalize" an
+application.
+
+See the Sandbox for other MailReader examples using other architectures.
+
+* http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2/apps/
+
+For more about the MailReader applicaton genneraly, visit Struts University.
+
+* http://www.StrutsUniversity.org/
+
+I18N:
+=====
+Please note that this project was created with the assumption that it will be run
+in an environment where the default locale is set to English. This means that
+the default messages defined in package.properties are in English. If the default
+locale for your server is different, then rename package.properties to package_en.properties
+and create a new package.properties with proper values for your default locale.
diff --git a/mailreader2/pom.xml b/mailreader2/pom.xml
new file mode 100644
index 0000000..8e2da35
--- /dev/null
+++ b/mailreader2/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.struts</groupId>
+ <artifactId>struts-examples</artifactId>
+ <version>1.0.0</version>
+ </parent>
+
+ <artifactId>mailreader2</artifactId>
+ <packaging>war</packaging>
+ <name>Struts 2 Mail Reader Webapp</name>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>commons-digester</groupId>
+ <artifactId>commons-digester</artifactId>
+ <version>2.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>3.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-maven-plugin</artifactId>
+ <version>${jetty-plugin.version}</version>
+ <configuration>
+ <webApp>
+ <contextPath>/${project.artifactId}</contextPath>
+ </webApp>
+ <stopKey>CTRL+C</stopKey>
+ <stopPort>8999</stopPort>
+ <scanIntervalSeconds>10</scanIntervalSeconds>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+</project>
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/ApplicationListener.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/ApplicationListener.java
new file mode 100644
index 0000000..2e1d604
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/ApplicationListener.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts.examples.mailreader2.dao.impl.memory.MemoryUserDatabase;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import java.io.*;
+
+/**
+ * <p><code>ServletContextListener</code> that initializes and finalizes the
+ * persistent storage of User and Subscription information for the Struts
+ * Demonstration Application, using an in-memory database backed by an XML
+ * file.</p>
+ * <p/>
+ * <p><strong>IMPLEMENTATION WARNING</strong> - 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
+ * <strong>NOT</strong> assume that files written here will survive a restart
+ * of your servlet container.</p>
+ * <p/>
+ * <p>This class was borrowed from the Shale Mailreader. Changes were:</p>
+ * <p/>
+ * <ul>
+ * <p/>
+ * <li>Path to database.xml (under classes here). </li>
+ * <p/>
+ * <li>Class to store protocol list (an array here). </li>
+ * <p/>
+ * </ul>
+ * <p>
+ * DEVELOPMENT NOTE - Another approach would be to instantiate the database via Spring.
+ * </p>
+ */
+public final class ApplicationListener implements ServletContextListener {
+
+ /**
+ * <p>Appication scope attribute key under which the in-memory version of
+ * our database is stored.</p>
+ */
+ public static final String DATABASE_KEY = "database";
+
+ /**
+ * <p>Application scope attribute key under which the valid selection
+ * items for the protocol property is stored.</p>
+ */
+ public static final String PROTOCOLS_KEY = "protocols";
+
+ /**
+ * <p>The <code>ServletContext</code> for this web application.</p>
+ */
+ private ServletContext context = null;
+
+ /**
+ * The {@link MemoryUserDatabase} object we construct and make available.
+ */
+ private MemoryUserDatabase database = null;
+
+ /**
+ * <p>Logging output for this plug in instance.</p>
+ */
+ private Logger log = LogManager.getLogger(this.getClass());
+
+ /**
+ * <p>The web application resource path of our persistent database storage
+ * file.</p>
+ */
+ private String pathname = "/WEB-INF/database.xml";
+
+ /**
+ * <p>Return the application resource path to the database.</p>
+ *
+ * @return application resource path path to the database
+ */
+ public String getPathname() {
+ return (this.pathname);
+ }
+
+ /**
+ * <p>Set the application resource path to the database.</p>
+ *
+ * @param pathname to the database
+ */
+ public void setPathname(String pathname) {
+ this.pathname = pathname;
+ }
+
+ /**
+ * <p>Gracefully shut down this database, releasing any resources that
+ * were allocated at initialization.</p>
+ *
+ * @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;
+ }
+
+ /**
+ * <p>Initialize and load our initial database from persistent
+ * storage.</p>
+ *
+ * @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);
+ }
+
+ /**
+ * <p>Calculate and return an absolute pathname to the XML file to contain
+ * our persistent storage information.</p>
+ *
+ * @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/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/AuthenticationInterceptor.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/AuthenticationInterceptor.java
new file mode 100644
index 0000000..bb0b518
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/AuthenticationInterceptor.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts.examples.mailreader2;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.interceptor.Interceptor;
+import org.apache.struts.examples.mailreader2.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<String, Object> 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/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/Constants.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/Constants.java
new file mode 100644
index 0000000..a5beecd
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/Constants.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+/**
+ * <p> Manifest constants for the MailReader application. </p>
+ */
+public final class Constants {
+
+ // --- Tokens ----
+
+ /**
+ * <p> The token representing a "cancel" request. </p>
+ */
+ public static final String CANCEL = "cancel";
+
+ /**
+ * <p> The token representing a "create" task. </p>
+ */
+ public static final String CREATE = "Create";
+
+ /**
+ * <p> The application scope attribute under which our user database is
+ * stored. </p>
+ */
+ public static final String DATABASE_KEY = "database";
+
+ /**
+ * <p> The token representing a "edit" task. </p>
+ */
+ public static final String DELETE = "Delete";
+
+ /**
+ * <p> The token representing a "edit" task. </p>
+ */
+ public static final String EDIT = "Edit";
+
+ /**
+ * <p> The package name for this application. </p>
+ */
+ public static final String PACKAGE = "org.apache.struts.apps.mailreader";
+
+ /**
+ * <p> The session scope attribute under which the Subscription object
+ * currently selected by our logged-in User is stored. </p>
+ */
+ public static final String SUBSCRIPTION_KEY = "subscription";
+
+ /**
+ * <p> The session scope attribute under which the User object for the
+ * currently logged in user is stored. </p>
+ */
+ public static final String USER_KEY = "user";
+
+ /**
+ * <p>The token representing the "Host" property.
+ */
+ public static final String HOST = "host";
+
+
+ // ---- Error Messages ----
+
+ /**
+ * <p>
+ * A static message in case message resource is not loaded.
+ * </p>
+ */
+ public static final String ERROR_MESSAGES_NOT_LOADED =
+ "ERROR: Message resources not loaded -- check servlet container logs for error messages.";
+
+ /**
+ * <p>
+ * A static message in case database resource is not loaded.
+ * <p>
+ */
+ public static final String ERROR_DATABASE_NOT_LOADED =
+ "ERROR: User database not loaded -- check servlet container logs for error messages.";
+
+ /**
+ * <p>
+ * A standard key from the message resources file, to test if it is available.
+ * <p>
+ */
+ public static final String ERROR_DATABASE_MISSING = "error.database.missing";
+
+ /**
+ * <P>
+ * A "magic" username to trigger an ExpiredPasswordException for testing.
+ *</p>
+ */
+ public static final String EXPIRED_PASSWORD_EXCEPTION = "ExpiredPasswordException";
+
+ /**
+ * <p>
+ * Name of field to associate with authentification errors.
+ * <p>
+ */
+ public static final String PASSWORD_MISMATCH_FIELD = "password";
+
+ // ---- Log Messages ----
+
+ /**
+ * <p> Message to log if saving a user fails. </p>
+ */
+ public static final String LOG_DATABASE_SAVE_ERROR =
+ " Unexpected error when saving User: ";
+
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/LoginAction.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/LoginAction.java
new file mode 100644
index 0000000..6ec7da0
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/LoginAction.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+import org.apache.struts.examples.mailreader2.dao.ExpiredPasswordException;
+import org.apache.struts.examples.mailreader2.dao.User;
+
+/**
+ * <p> Validate a user login. </p>
+ */
+public final class LoginAction 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/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/LogoutAction.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/LogoutAction.java
new file mode 100644
index 0000000..c7464b9
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/LogoutAction.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+/**
+ * <p> Log user out of the current session. </p>
+ */
+public class LogoutAction extends MailreaderSupport {
+
+ public String execute() {
+ setUser(null);
+ return SUCCESS;
+ }
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/MailreaderSupport.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/MailreaderSupport.java
new file mode 100644
index 0000000..2e8d1f7
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/MailreaderSupport.java
@@ -0,0 +1,578 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+import com.opensymphony.xwork2.ActionSupport;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts.examples.mailreader2.dao.ExpiredPasswordException;
+import org.apache.struts.examples.mailreader2.dao.Subscription;
+import org.apache.struts.examples.mailreader2.dao.User;
+import org.apache.struts.examples.mailreader2.dao.UserDatabase;
+import org.apache.struts.examples.mailreader2.dao.impl.memory.MemorySubscription;
+import org.apache.struts.examples.mailreader2.dao.impl.memory.MemoryUser;
+import org.apache.struts2.interceptor.ApplicationAware;
+import org.apache.struts2.interceptor.SessionAware;
+
+import java.util.Map;
+
+/**
+ * <p> Base Action for MailreaderSupport application. </p>
+ * <p/>
+ * <p> 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.</p>
+ */
+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;
+ }
+
+ /**
+ * Convenience method to copy User properties.
+ **/
+ protected void copyUser(User source, User target) {
+ if ((source==null) || (target==null)) return;
+ target.setFromAddress(source.getFromAddress());
+ target.setFullName(source.getFullName());
+ target.setPassword(source.getPassword());
+ target.setReplyToAddress(source.getReplyToAddress());
+ }
+
+ /**
+ * Convenience method to copy Subscription properties.
+ **/
+ protected void copySubscription(Subscription source, Subscription target) {
+ if ((source==null) || (target==null)) return;
+ target.setAutoConnect(source.getAutoConnect());
+ target.setPassword(source.getPassword());
+ target.setType(source.getType());
+ target.setUsername(source.getUsername());
+ }
+
+
+ // ---- ApplicationAware ----
+
+ /**
+ * <p>Field to store application context or its proxy.</p>
+ * <p/>
+ * <p>The application context lasts for the life of the application. A
+ * reference to the database is stored in the application context at
+ * startup.</p>
+ */
+ private Map application;
+
+ /**
+ * <p>Store a new application context.</p>
+ *
+ * @param value A Map representing application state
+ */
+ public void setApplication(Map value) {
+ application = value;
+ }
+
+ /**
+ * <p>Provide application context.</p>
+ */
+ public Map getApplication() {
+ return application;
+ }
+
+ // ---- SessionAware ----
+
+ /**
+ * <p>Field to store session context, or its proxy.</p>
+ */
+ private Map session;
+
+ /**
+ * <p>Store a new session context.</p>
+ *
+ * @param value A Map representing session state
+ */
+ public void setSession(Map value) {
+ session = value;
+ }
+
+ /**
+ * <p>Provide session context.</p>
+ *
+ * @return session context
+ */
+ public Map getSession() {
+ return session;
+ }
+
+ // ---- Task property (utilized by UI) ----
+
+ /**
+ * <p>Field to store workflow task.</p>
+ * <p/>
+ * <p>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.</p>
+ */
+ private String task = null;
+
+
+ /**
+ * <p>Provide worklow task.</p>
+ *
+ * @return Returns the task.
+ */
+ public String getTask() {
+ return task;
+ }
+
+ /**
+ * <p>Store new workflow task.</p>
+ *
+ * @param value The task to set.
+ */
+ public void setTask(String value) {
+ task = value;
+ }
+
+ // ---- Token property (utilized by UI) ----
+
+ /**
+ * <p>Field to store double-submit guard.</p>
+ */
+ private String token = null;
+
+
+ /**
+ * <p>Provide Token.</p>
+ *
+ * @return Returns the token.
+ */
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * <p>Store new Token.</p>
+ *
+ * @param value The token to set.
+ */
+ public void setToken(String value) {
+ token = value;
+ }
+
+
+ // ---- Host property ----
+
+ /**
+ * <p>Field to store Subscription host.</p>
+ * <p/>
+ * <p> The host is an immutable property of the Subscrtion DAP object, so
+ * we need to store it locally until we are ready to create the
+ * Subscription. </p>
+ */
+ private String host;
+
+ /**
+ * <p>Provide tSubscription host.</p>
+ *
+ * @return host property
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * <p>Store new Subscription host.</p>
+ *
+ * @param value
+ */
+ public void setHost(String value) {
+ host = value;
+ }
+
+ // ---- Password property ----
+
+ /**
+ * <p>Field to store User password property.</p>
+ * <p/>
+ * <p>The User DAO object password proerty is immutable, so we store it
+ * locally until we are ready to create the object.</p>
+ */
+ private String password = null;
+
+
+ /**
+ * <p>Provide User password</p>
+ *
+ * @return Returns the password.
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * <p>Store new User Password</p>
+ *
+ * @param value The password to set.
+ */
+ public void setPassword(String value) {
+ password = value;
+ }
+
+ // ---- Password2 property (confirmation) ----
+
+ /**
+ * <p>Field to store the User password confirmation.</p>
+ * <p/>
+ * <p>When a User object is created, we ask the client to enter the
+ * password twice, to help ensure the password is being typed
+ * correctly.</p>
+ */
+ private String password2 = null;
+
+
+ /**
+ * <p>Provide the User password confirmation.</p>
+ *
+ * @return Returns the confirmationpassword.
+ */
+ public String getPassword2() {
+ return password2;
+ }
+
+ /**
+ * <p>Store a new User password confirmation.</p>
+ *
+ * @param value The confirmation password to set.
+ */
+ public void setPassword2(String value) {
+ password2 = value;
+ }
+
+ // ---- Username property ----
+
+ /**
+ * <p>Field to store User username.</p>
+ * <p/>
+ * <p>The User DAO object password proerty is immutable, so we store it
+ * locally until we are ready to create the object.</p>
+ */
+ private String username = null;
+
+
+ /**
+ * <p>Provide User username.</p>
+ *
+ * @return Returns the User username.
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * <p>Store new User username</p>
+ *
+ * @param value The username to set.
+ */
+ public void setUsername(String value) {
+ username = value;
+ }
+
+ // ---- Database property ----
+
+ /**
+ * <p>Provide reference to UserDatabase, or null if the database is not
+ * available. </p>
+ *
+ * @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;
+ }
+
+ /**
+ * <p>Store a new reference to UserDatabase</p>
+ *
+ * @param database
+ */
+ public void setDatabase(UserDatabase database) {
+ getApplication().put(Constants.DATABASE_KEY, database);
+ }
+
+ // ---- User property ----
+
+ /**
+ * <p>Provide reference to User object for authenticated user.</p>
+ *
+ * @return User object for authenticated user.
+ */
+ public User getUser() {
+ return (User) getSession().get(Constants.USER_KEY);
+ }
+
+ /**
+ * <p>Store new reference to User Object.</p>
+ *
+ * @param user User object for authenticated user
+ */
+ public void setUser(User user) {
+ getSession().put(Constants.USER_KEY, user);
+ }
+
+ /**
+ * <p>Obtain User object from database, or return null if the credentials
+ * are not found or invalid.</p>
+ *
+ * @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;
+ }
+
+ /**
+ * <p><code>Log</code> instance for this application. </p>
+ */
+ protected Logger log = LogManager.getLogger(Constants.PACKAGE);
+
+ /**
+ * <p> Persist the User object, including subscriptions, to the database.
+ * </p>
+ *
+ * @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);
+ }
+
+ /**
+ * <p> Verify input for creating a new user, create the user, and process
+ * the login. </p>
+ *
+ * @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
+
+ /**
+ * <p>Use the current User object to create a new User object, and make
+ * the new User object the authenticated user.</p>
+ * <p/>
+ * <p>The "current" User object is usually a temporary object being used
+ * to capture input.</p>
+ *
+ * @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) {
+ copyUser(input,user);
+ setUser(user);
+ }
+ }
+
+ // ---- Subscription property ----
+
+ /**
+ * <p>Obtain the cached Subscription object, if any. </p>
+ *
+ * @return Cached Subscription object or null
+ */
+ public Subscription getSubscription() {
+ return (Subscription) getSession().get(Constants.SUBSCRIPTION_KEY);
+ }
+
+ /**
+ * <p>Store new User Subscription.</p>
+ *
+ * @param subscription
+ */
+ public void setSubscription(Subscription subscription) {
+ getSession().put(Constants.SUBSCRIPTION_KEY, subscription);
+ }
+
+ /**
+ * <p> Obtain User Subscription object for the given host, or return null
+ * if not found. </p>
+ *
+ * <p>It would be possible for this code to throw a NullPointerException,
+ * but the ExceptionHandler in the xwork.xml will catch that for us.</p>
+ *
+ * @return The matching Subscription or null
+ */
+ public Subscription findSubscription(String host) {
+ return getUser().findSubscription(host);
+ }
+
+ /**
+ * <p>Obtain uSER Subscription for the local Host property.</p>
+ * <p/>
+ * <p>Usually, the host property will be set from the client request,
+ * because it was embedded in a link to the Subcription action.
+ *
+ * @return Subscription or null if not found
+ */
+ public Subscription findSubscription() {
+ return findSubscription(getHost());
+ }
+
+ /**
+ * <p>Provide a "temporary" User Subscription object that can be used to
+ * capture input values.</p>
+ */
+ public void createInputSubscription() {
+ Subscription sub = new MemorySubscription(getUser(), null);
+ setSubscription(sub);
+ setHost(sub.getHost());
+ }
+
+ /**
+ * <p>Provide new User Subscription object for the given host, or null if
+ * the host is not unique.</p>
+ *
+ * @param host
+ * @return New User Subscription object or null
+ */
+ public Subscription createSubscription(String host) {
+
+ Subscription sub;
+
+ sub = findSubscription(host);
+
+ if (null != sub) {
+ // FIXME - localization - "error.host.unique")
+ addFieldError(Constants.HOST,"That hostname is already defined");
+ return null;
+ }
+
+ return getUser().createSubscription(host);
+ }
+
+ /**
+ * <p>Create a new Subscription from the current Subscription object,
+ * making the new Subscription the current Subscription. </p>
+ * <p/>
+ * <p>Usually, the "current" Subscription is a temporary object being used
+ * to capture input values.</p>
+ *
+ * @param host
+ */
+ public void copySubscription(String host) {
+ Subscription input = getSubscription();
+ Subscription sub = createSubscription(host);
+ if (null != sub) {
+ copySubscription(input, sub);
+ setSubscription(sub);
+ setHost(sub.getHost());
+ }
+ }
+
+ /**
+ * <p>Delete the current Subscription object from the database.</p>
+ */
+ public void removeSubscription() {
+ getUser().removeSubscription(getSubscription());
+ getSession().remove(Constants.SUBSCRIPTION_KEY);
+ }
+
+ /**
+ * <p>Provide MailServer Host for current User Subscription.</p>
+ *
+ * @return MailServer Host for current User Subscription
+ */
+ public String getSubscriptionHost() {
+ Subscription sub = getSubscription();
+ if (null == sub) {
+ return null;
+ }
+ return sub.getHost();
+ }
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/RegistrationAction.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/RegistrationAction.java
new file mode 100644
index 0000000..bdf1a72
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/RegistrationAction.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+import org.apache.struts.examples.mailreader2.dao.User;
+
+/**
+ * <p>Insert or update a User object to the persistent store. </p>
+ */
+public class RegistrationAction extends MailreaderSupport {
+
+ /**
+ * <p>Double check that there is not a valid User login. </p>
+ *
+ * @return True if there is not a valid User login
+ */
+ private boolean isCreating() {
+ User user = getUser();
+ return (null == user) || (null == user.getDatabase());
+ }
+
+ /**
+ * <p> Retrieve User object to edit or null if User does not exist. </p>
+ *
+ * @return The "Success" result for this mapping
+ * @throws Exception on any error
+ */
+ public String input() throws Exception {
+
+ if (isCreating()) {
+ createInputUser();
+ setTask(Constants.CREATE);
+ } else {
+ setTask(Constants.EDIT);
+ setUsername(getUser().getUsername());
+ setPassword(getUser().getPassword());
+ setPassword2(getUser().getPassword());
+ }
+
+ return INPUT;
+ }
+
+ /**
+ * <p>Insert or update a Registration.</p>
+ *
+ * @return The "outcome" result code
+ * @throws Exception on any error
+ */
+ public String save() throws Exception {
+ return execute();
+ }
+
+ /**
+ * <p> Insert or update a User object to the persistent store. </p>
+ * <p/>
+ * <p> If a User is not logged in, then a new User is created and
+ * automatically logged in. Otherwise, the existing User is updated. </p>
+ *
+ * @return The "outcome" result code
+ * @throws Exception on any error
+ */
+ public String execute()
+ throws Exception {
+
+ boolean creating = Constants.CREATE.equals(getTask());
+ creating = creating && isCreating(); // trust but verify
+
+ if (creating) {
+
+ User user = findUser(getUsername(), getPassword());
+ boolean haveUser = (user != null);
+
+ if (haveUser) {
+ addActionError(getText("error.username.unique"));
+ return INPUT;
+ }
+
+ copyUser(getUsername(), getPassword());
+
+ } else {
+
+ // FIXME: Any way to call the RegisrationSave validators from here?
+ String newPassword = getPassword();
+ if (newPassword != null) {
+ String confirmPassword = getPassword2();
+ boolean matches = ((null != confirmPassword)
+ && (confirmPassword.equals(newPassword)));
+ if (matches) {
+ getUser().setPassword(newPassword);
+ } else {
+ addActionError(getText("error.password.match"));
+ return INPUT;
+ }
+ }
+ }
+
+ saveUser();
+
+ return SUCCESS;
+ }
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/SubscriptionAction.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/SubscriptionAction.java
new file mode 100644
index 0000000..e4a5053
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/SubscriptionAction.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+import com.opensymphony.xwork2.Preparable;
+import org.apache.struts.examples.mailreader2.dao.Subscription;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * <p> Provide an Edit method for retrieving an existing subscription, and a
+ * Save method for updating or inserting a subscription. </p>
+ */
+public class SubscriptionAction extends MailreaderSupport implements Preparable {
+
+ /**
+ * <p>Field to store list of MailServer types</p>
+ */
+ private Map<String, String> types = null;
+
+ /**
+ * <p>Provide the list of MailServer types.</p>
+ *
+ * @return List of MailServer types
+ */
+ public Map<String, String> getTypes() {
+ return types;
+ }
+
+ /**
+ * <p>Setup the MailerServer types and set the local Host property from
+ * the User Subscription (if any). </p>
+ */
+ public void prepare() {
+
+ Map<String, String> m = new LinkedHashMap<>();
+ m.put("imap", "IMAP Protocol");
+ m.put("pop3", "POP3 Protocol");
+ types = m;
+
+ setHost(getSubscriptionHost());
+ }
+
+ /**
+ * <p>Setup a temporary User Subscription object to capture input
+ * values.</p>
+ *
+ * @return INPUT
+ */
+ public String input() {
+ createInputSubscription();
+ setTask(Constants.CREATE);
+ return INPUT;
+ }
+
+ /**
+ * <p>Load User Subscription for the local Host property.</p>
+ * <p/>
+ * <p>Usually, the Host is being set from the request by a link to an Edit
+ * or Delete task.</p>
+ *
+ * @return INPUT or Error, if Subscription is not found
+ */
+ public String find() {
+ Subscription sub = findSubscription();
+ if (sub == null) {
+ return ERROR;
+ }
+
+ setSubscription(sub);
+ return INPUT;
+ }
+
+ /**
+ * <p>Prepare to present a confirmation page before removing
+ * Subscription.</p>
+ *
+ * @return INPUT or Error, if Subscription is not found
+ */
+ public String delete() {
+ setTask(Constants.DELETE);
+ return find();
+ }
+
+ /**
+ * <p>Prepare to edit User Subscription.</p>
+ *
+ * @return INPUT or Error, if Subscription is not found
+ */
+ public String edit() {
+ setTask(Constants.EDIT);
+ return find();
+ }
+
+ /**
+ * <p> Examine the Task property and DELETE, CREATE, or save the User
+ * Subscription, as appropriate. </p>
+ *
+ * @return SUCCESS
+ * @throws Exception on a database error
+ */
+ public String save() throws Exception {
+ if (Constants.DELETE.equals(getTask())) {
+ removeSubscription();
+ }
+
+ if (Constants.CREATE.equals(getTask())) {
+ copySubscription(getHost());
+ }
+
+ if (hasErrors()) return INPUT;
+
+ saveUser();
+ return SUCCESS;
+ }
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/WelcomeAction.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/WelcomeAction.java
new file mode 100644
index 0000000..fd31373
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/WelcomeAction.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2;
+
+/**
+ * Verify that essential resources are available.
+ */
+public class WelcomeAction extends MailreaderSupport {
+
+ public String execute() {
+
+ // Confirm message resources loaded
+ String message = getText(Constants.ERROR_DATABASE_MISSING);
+ if (Constants.ERROR_DATABASE_MISSING.equals(message)) {
+ addActionError(Constants.ERROR_MESSAGES_NOT_LOADED);
+ }
+
+ // Confirm database loaded
+ if (null==getDatabase()) {
+ addActionError(Constants.ERROR_DATABASE_NOT_LOADED);
+ }
+
+ if (hasErrors()) {
+ return ERROR;
+ }
+ else {
+ return SUCCESS;
+ }
+ }
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/ExpiredPasswordException.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/ExpiredPasswordException.java
new file mode 100644
index 0000000..3c397ea
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/ExpiredPasswordException.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2.dao;
+
+/**
+ * Example of an application-specific exception for which a handler
+ * can be configured.
+ */
+public class ExpiredPasswordException extends Exception {
+
+ /**
+ * Construct a new instance of this exception for the specified username.
+ *
+ * @param username Username whose password has expired
+ */
+ public ExpiredPasswordException(String username) {
+ super("Password for " + username + " has expired.");
+ }
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/Subscription.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/Subscription.java
new file mode 100644
index 0000000..64015d5
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/Subscription.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2.dao;
+
+/**
+ * <p>A <strong>Subscription</strong> which is stored, along with the
+ * associated {@link User}, in a {@link UserDatabase}.</p>
+ *
+ * @version $Rev$ $Date$
+ */
+public interface Subscription {
+
+ // ------------------------------------------------------------- Properties
+
+ /**
+ * Return the auto-connect flag.
+ */
+ boolean getAutoConnect();
+
+ /**
+ * Set the auto-connect flag.
+ *
+ * @param autoConnect The new auto-connect flag
+ */
+ void setAutoConnect(boolean autoConnect);
+
+ /**
+ * Return the host name.
+ */
+ String getHost();
+
+ /**
+ * Return the password.
+ */
+ String getPassword();
+
+ /**
+ * Set the password.
+ *
+ * @param password The new password
+ */
+ void setPassword(String password);
+
+ /**
+ * Return the subscription type.
+ */
+ String getType();
+
+ /**
+ * Set the subscription type.
+ *
+ * @param type The new subscription type
+ */
+ void setType(String type);
+
+ /**
+ * Return the {@link User} owning this Subscription.
+ */
+ User getUser();
+
+ /**
+ * Return the username.
+ */
+ String getUsername();
+
+ /**
+ * Set the username.
+ *
+ * @param username The new username
+ */
+ void setUsername(String username);
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/User.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/User.java
new file mode 100644
index 0000000..6dd9124
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/User.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2.dao;
+
+/**
+ * <p>A <strong>User</strong> which is stored, along with his or her
+ * associated {@link Subscription}s, in a {@link UserDatabase}.</p>
+ */
+public interface User {
+
+ // ------------------------------------------------------------- Properties
+
+ /**
+ * Return the {@link UserDatabase} with which we are associated.
+ */
+ UserDatabase getDatabase();
+
+ /**
+ * Return the from address.
+ */
+ String getFromAddress();
+
+ /**
+ * Set the from address.
+ *
+ * @param fromAddress The new from address
+ */
+ void setFromAddress(String fromAddress);
+
+ /**
+ * Return the full name.
+ */
+ String getFullName();
+
+ /**
+ * Set the full name.
+ *
+ * @param fullName The new full name
+ */
+ void setFullName(String fullName);
+
+ /**
+ * Return the password.
+ */
+ String getPassword();
+
+ /**
+ * Set the password.
+ *
+ * @param password The new password
+ */
+ void setPassword(String password);
+
+ /**
+ * Return the reply-to address.
+ */
+ String getReplyToAddress();
+
+ /**
+ * Set the reply-to address.
+ *
+ * @param replyToAddress The new reply-to address
+ */
+ void setReplyToAddress(String replyToAddress);
+
+ /**
+ * Find and return all {@link Subscription}s associated with this user.
+ * If there are none, a zero-length array is returned.
+ */
+ Subscription[] getSubscriptions();
+
+ /**
+ * Return the username.
+ */
+ String getUsername();
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * Create and return a new {@link Subscription} associated with this
+ * User, for the specified host name.
+ *
+ * @param host Host name for which to create a subscription
+ *
+ * @exception IllegalArgumentException if the host name is not unique
+ * for this user
+ */
+ Subscription createSubscription(String host);
+
+ /**
+ * Find and return the {@link Subscription} associated with the specified
+ * host. If none is found, return <code>null</code>.
+ *
+ * @param host Host name to look up
+ */
+ Subscription findSubscription(String host);
+
+ /**
+ * Remove the specified {@link Subscription} from being associated
+ * with this User.
+ *
+ * @param subscription Subscription to be removed
+ *
+ * @exception IllegalArgumentException if the specified subscription is not
+ * associated with this User
+ */
+ void removeSubscription(Subscription subscription);
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/UserDatabase.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/UserDatabase.java
new file mode 100644
index 0000000..154e66e
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/UserDatabase.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts.examples.mailreader2.dao;
+
+/**
+ * <p>A <strong>Data Access Object</strong> (DAO) interface describing
+ * the available operations for retrieving and storing {@link User}s
+ * (and their associated {@link Subscription}s) in some persistence layer
+ * whose characteristics are not specified here. One or more implementations
+ * will be created to perform the actual I/O that is required.</p>
+ */
+public interface UserDatabase {
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * <p>Create and return a new {@link User} defined in this user database.
+ * </p>
+ *
+ * @param username Username of the new user
+ *
+ * @exception IllegalArgumentException if the specified username
+ * is not unique
+ */
+ User createUser(String username);
+
+ /**
+ * <p>Finalize access to the underlying persistence layer.</p>
+ *
+ * @exception Exception if a database access error occurs
+ */
+ void close() throws Exception;
+
+ /**
+ * <p>Return the existing {@link User} with the specified username,
+ * if any; otherwise return <code>null</code>.</p>
+ *
+ * @param username Username of the user to retrieve
+ * @throws ExpiredPasswordException if user password has expired
+ * and must be changed
+ */
+ User findUser(String username) throws ExpiredPasswordException;
+
+ /**
+ * <p>Return the set of {@link User}s defined in this user database.</p>
+ */
+ User[] findUsers();
+
+ /**
+ * <p>Return true if open() has been called.</p>
+ *
+ * @exception Exception if a database access error occurs
+ */
+ boolean isOpen();
+
+ /**
+ * <p>Initiate access to the underlying persistence layer.</p>
+ *
+ * @exception Exception if a database access error occurs
+ */
+ void open() throws Exception;
+
+ /**
+ * Remove the specified {@link User} from this database.
+ *
+ * @param user User to be removed
+ *
+ * @exception IllegalArgumentException if the specified user is not
+ * associated with this database
+ */
+ void removeUser(User user);
+
+ /**
+ * <p>Save any pending changes to the underlying persistence layer.</p>
+ *
+ * @exception Exception if a database access error occurs
+ */
+ void save() throws Exception;
+
+}
\ No newline at end of file
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/AbstractSubscription.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/AbstractSubscription.java
new file mode 100644
index 0000000..c327c8d
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/AbstractSubscription.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package org.apache.struts.examples.mailreader2.dao.impl;
+
+import org.apache.struts.examples.mailreader2.dao.Subscription;
+import org.apache.struts.examples.mailreader2.dao.User;
+
+/**
+ * <p>Concrete implementation of {@link AbstractSubscription}.</p>
+ *
+ * @version $Rev$
+ * @since Struts 1.1
+ */
+
+public class AbstractSubscription implements Subscription {
+
+
+ // ----------------------------------------------------------- Constructors
+
+
+ /**
+ * <p>Construct a new Subscription associated with the specified
+ * {@link User}.
+ *
+ * @param user The user with which we are associated
+ * @param host The mail host for this subscription
+ */
+ public AbstractSubscription(User user, String host) {
+
+ super();
+ this.user = user;
+ this.host = host;
+
+ }
+
+
+ // ----------------------------------------------------- Instance Variables
+
+
+ /**
+ * The mail host for this subscription.
+ */
+ private String host = null;
+
+
+ /**
+ * The {@link User} with which we are associated.
+ */
+ private User user = null;
+
+
+ // ------------------------------------------------------------- Properties
+
+
+ /**
+ * Should we auto-connect at startup time?
+ */
+ private boolean autoConnect = false;
+
+ public boolean getAutoConnect() {
+ return (this.autoConnect);
+ }
+
+ public void setAutoConnect(boolean autoConnect) {
+ this.autoConnect = autoConnect;
+ }
+
+
+ /**
+ * The mail host for this subscription.
+ */
+ public String getHost() {
+ return (this.host);
+ }
+
+
+ /**
+ * The password (in clear text) for this subscription.
+ */
+ private String password = null;
+
+ public String getPassword() {
+ return (this.password);
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+
+ /**
+ * The subscription type ("imap" or "pop3").
+ */
+ private String type = "imap";
+
+ public String getType() {
+ return (this.type);
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ /**
+ * The User owning this Subscription.
+ */
+ public User getUser() {
+ return (this.user);
+ }
+
+
+ /**
+ * The username for this subscription.
+ */
+ private String username = null;
+
+ public String getUsername() {
+ return (this.username);
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+
+ // --------------------------------------------------------- Public Methods
+
+
+ /**
+ * Return a String representation of this object.
+ */
+ public String toString() {
+
+ StringBuffer sb = new StringBuffer("<subscription host=\"");
+ sb.append(host);
+ sb.append("\" autoConnect=\"");
+ sb.append(autoConnect);
+ sb.append("\"");
+ if (password != null) {
+ sb.append(" password=\"");
+ sb.append(password);
+ sb.append("\"");
+ }
+ if (type != null) {
+ sb.append(" type=\"");
+ sb.append(type);
+ sb.append("\"");
+ }
+ if (username != null) {
+ sb.append(" username=\"");
+ sb.append(username);
+ sb.append("\"");
+ }
+ sb.append(">");
+ return (sb.toString());
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/AbstractUser.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/AbstractUser.java
new file mode 100644
index 0000000..f01c90b
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/AbstractUser.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package org.apache.struts.examples.mailreader2.dao.impl;
+
+import org.apache.struts.examples.mailreader2.dao.Subscription;
+import org.apache.struts.examples.mailreader2.dao.User;
+import org.apache.struts.examples.mailreader2.dao.UserDatabase;
+
+import java.util.HashMap;
+
+/**
+ * <p>Concrete implementation of {@link AbstractUser}.</p>
+ *
+ * @version $Rev$
+ * @since Struts 1.1
+ */
+
+public abstract class AbstractUser implements User {
+
+
+ // ----------------------------------------------------------- Constructors
+
+
+ /**
+ * <p>Construct a new User associated with the specified
+ * {@link UserDatabase}.
+ *
+ * @param database The user database with which we are associated
+ * @param username The username of this user
+ */
+ public AbstractUser(UserDatabase database, String username) {
+
+ super();
+ this.database = database;
+ this.username = username;
+
+ }
+
+
+ // ----------------------------------------------------- Instance Variables
+
+
+ /**
+ * The {@link UserDatabase} with which we are associated.
+ */
+ private UserDatabase database = null;
+
+
+ /**
+ * The {@link Subscription}s for this User, keyed by hostname.
+ */
+ private HashMap subscriptions = new HashMap();
+
+
+ /**
+ * The username for this user.
+ */
+ private String username = null;
+
+
+ // ------------------------------------------------------------- Properties
+
+
+ /**
+ * The {@link UserDatabase} with which we are associated.
+ */
+ public UserDatabase getDatabase() {
+ return (this.database);
+ }
+
+
+ /**
+ * The email address from which messages are sent.
+ */
+ private String fromAddress = null;
+
+ public String getFromAddress() {
+ return (this.fromAddress);
+ }
+
+ public void setFromAddress(String fromAddress) {
+ this.fromAddress = fromAddress;
+ }
+
+
+ /**
+ * The full name of this user, included in from addresses.
+ */
+ private String fullName = null;
+
+ public String getFullName() {
+ return (this.fullName);
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+
+ /**
+ * The password (in clear text).
+ */
+ private String password = null;
+
+ public String getPassword() {
+ return (this.password);
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+
+ /**
+ * The EMAIL address to which replies should be sent.
+ */
+ private String replyToAddress = null;
+
+ public String getReplyToAddress() {
+ return (this.replyToAddress);
+ }
+
+ public void setReplyToAddress(String replyToAddress) {
+ this.replyToAddress = replyToAddress;
+ }
+
+
+ /**
+ * Find and return all {@link Subscription}s associated with this user.
+ * If there are none, a zero-length array is returned.
+ */
+ public Subscription[] getSubscriptions() {
+
+ synchronized (subscriptions) {
+ Subscription results[] = new Subscription[subscriptions.size()];
+ return ((Subscription[]) subscriptions.values().toArray(results));
+ }
+
+ }
+
+
+ /**
+ * The username (must be unique).
+ */
+ public String getUsername() {
+ return (this.username);
+ }
+
+
+ // --------------------------------------------------------- Public Methods
+
+
+ /**
+ * Create and return a new {@link Subscription} associated with this
+ * User, for the specified host name.
+ *
+ * @param host Host name for which to create a subscription
+ *
+ * @exception IllegalArgumentException if the host name is not unique
+ * for this user
+ */
+ public Subscription createSubscription(String host) {
+
+ synchronized (subscriptions) {
+ if (subscriptions.get(host) != null) {
+ throw new IllegalArgumentException("Duplicate host '" + host
+ + "' for user '" +
+ username + "'");
+ }
+ Subscription subscription =
+ new AbstractSubscription(this, host);
+ synchronized (subscriptions) {
+ subscriptions.put(host, subscription);
+ }
+ return (subscription);
+ }
+
+ }
+
+
+ /**
+ * Find and return the {@link Subscription} associated with the specified
+ * host. If none is found, return <code>null</code>.
+ *
+ * @param host Host name to look up
+ */
+ public Subscription findSubscription(String host) {
+
+ synchronized (subscriptions) {
+ return ((Subscription) subscriptions.get(host));
+ }
+
+ }
+
+
+ /**
+ * Remove the specified {@link Subscription} from being associated
+ * with this User.
+ *
+ * @param subscription Subscription to be removed
+ *
+ * @exception IllegalArgumentException if the specified subscription is not
+ * associated with this User
+ */
+ public void removeSubscription(Subscription subscription) {
+
+ if (!(this == subscription.getUser())) {
+ throw new IllegalArgumentException
+ ("Subscription not associated with this user");
+ }
+ synchronized (subscriptions) {
+ subscriptions.remove(subscription.getHost());
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemorySubscription.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemorySubscription.java
new file mode 100644
index 0000000..843a491
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemorySubscription.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.struts.examples.mailreader2.dao.impl.memory;
+
+import org.apache.struts.examples.mailreader2.dao.User;
+import org.apache.struts.examples.mailreader2.dao.impl.AbstractSubscription;
+
+public class MemorySubscription extends AbstractSubscription {
+
+ public MemorySubscription(User user, String host) {
+ super(user, host);
+ }
+
+}
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemoryUser.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemoryUser.java
new file mode 100644
index 0000000..544b775
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemoryUser.java
@@ -0,0 +1,71 @@
+/*
+ * $Id: $
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.struts.examples.mailreader2.dao.impl.memory;
+
+import org.apache.struts.examples.mailreader2.dao.UserDatabase;
+import org.apache.struts.examples.mailreader2.dao.impl.AbstractUser;
+
+/**
+ * <p>Concrete implementation of {@link AbstractUser} used for an in-memory
+ * database backed by an XML data file.</p>
+ *
+ * @version $Rev$
+ */
+public class MemoryUser extends AbstractUser {
+
+ public MemoryUser(UserDatabase database, String username) {
+ super(database, username);
+ }
+ /**
+ * Return a String representation of this object.
+ */
+ public String toString() {
+
+ StringBuffer sb = new StringBuffer("<user username=\"");
+ sb.append(getUsername());
+ sb.append("\"");
+ if (getFromAddress() != null) {
+ sb.append(" fromAddress=\"");
+ sb.append(getFromAddress());
+ sb.append("\"");
+ }
+ if (getFullName() != null) {
+ sb.append(" fullName=\"");
+ sb.append(getFullName());
+ sb.append("\"");
+ }
+ if (getPassword() != null) {
+ sb.append(" password=\"");
+ sb.append(getPassword());
+ sb.append("\"");
+ }
+ if (getReplyToAddress() != null) {
+ sb.append(" replyToAddress=\"");
+ sb.append(getReplyToAddress());
+ sb.append("\"");
+ }
+ sb.append(">");
+ return (sb.toString());
+
+ }
+
+}
\ No newline at end of file
diff --git a/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemoryUserDatabase.java b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemoryUserDatabase.java
new file mode 100644
index 0000000..a3da63d
--- /dev/null
+++ b/mailreader2/src/main/java/org/apache/struts/examples/mailreader2/dao/impl/memory/MemoryUserDatabase.java
@@ -0,0 +1,317 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts.examples.mailreader2.dao.impl.memory;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.HashMap;
+
+import org.apache.commons.digester.Digester;
+import org.apache.commons.digester.ObjectCreationFactory;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.struts.examples.mailreader2.dao.Subscription;
+import org.apache.struts.examples.mailreader2.dao.User;
+import org.apache.struts.examples.mailreader2.dao.UserDatabase;
+import org.xml.sax.Attributes;
+
+/**
+ * <p>Concrete implementation of {@link UserDatabase} for an in-memory
+ * database backed by an XML data file.</p>
+ */
+public class MemoryUserDatabase implements UserDatabase {
+
+ /**
+ * Logging output for this user database instance.
+ */
+ private Log log = LogFactory.getLog(this.getClass());
+
+ /**
+ * The {@link User}s associated with this UserDatabase, keyed by username.
+ */
+ private final HashMap<String, User> users = new HashMap<>();
+
+ private boolean open = false;
+
+ /**
+ * Absolute pathname to the persistent file we use for loading and storing
+ * persistent data.
+ */
+ private String pathname = null;
+
+ private String pathnameOld = null;
+
+ private String pathnameNew = null;
+
+ public String getPathname() {
+ return (this.pathname);
+ }
+
+ public void setPathname(String pathname) {
+ this.pathname = pathname;
+ pathnameOld = pathname + ".old";
+ pathnameNew = pathname + ".new";
+ }
+
+ public void close() throws Exception {
+ save();
+ this.open = false;
+ }
+
+ public User createUser(String username) {
+ synchronized (users) {
+ if (users.get(username) != null) {
+ throw new IllegalArgumentException("Duplicate user '" +
+ username + "'");
+ }
+ if (log.isTraceEnabled()) {
+ log.trace("Creating user '" + username + "'");
+ }
+ MemoryUser user = new MemoryUser(this, username);
+ synchronized (users) {
+ users.put(username, user);
+ }
+ return (user);
+ }
+ }
+
+ public User findUser(String username) {
+ synchronized (users) {
+ return ((User) users.get(username));
+ }
+ }
+
+ public User[] findUsers() {
+ synchronized (users) {
+ User[] results = new User[users.size()];
+ return users.values().toArray(results);
+ }
+ }
+
+ public void open() throws Exception {
+ FileInputStream fis;
+ BufferedInputStream bis = null;
+
+ try {
+ // Acquire an input stream to our database file
+ if (log.isDebugEnabled()) {
+ log.debug("Loading database from '" + pathname + "'");
+ }
+ fis = new FileInputStream(pathname);
+ bis = new BufferedInputStream(fis);
+
+ // Construct a digester to use for parsing
+ Digester digester = new Digester();
+ digester.push(this);
+ digester.setValidating(false);
+ digester.addFactoryCreate
+ ("database/user",
+ new MemoryUserCreationFactory(this));
+ digester.addFactoryCreate
+ ("database/user/subscription",
+ new MemorySubscriptionCreationFactory());
+
+ // Parse the input stream to initialize our database
+ digester.parse(bis);
+ bis.close();
+ bis = null;
+ fis = null;
+ this.open = true;
+
+ } catch (Exception e) {
+ log.error("Loading database from '" + pathname + "':", e);
+ throw e;
+ } finally {
+ if (bis != null) {
+ try {
+ bis.close();
+ } catch (Throwable t) {
+ // do nothing
+ }
+ }
+ }
+ }
+
+ public void removeUser(User user) {
+ if (!(this == user.getDatabase())) {
+ throw new IllegalArgumentException
+ ("User not associated with this database");
+ }
+ if (log.isTraceEnabled()) {
+ log.trace("Removing user '" + user.getUsername() + "'");
+ }
+ synchronized (users) {
+ users.remove(user.getUsername());
+ }
+ }
+
+ public void save() throws Exception {
+ if (log.isDebugEnabled()) {
+ log.debug("Saving database to '" + pathname + "'");
+ }
+ File fileNew = new File(pathnameNew);
+ PrintWriter writer = null;
+
+ try {
+
+ // Configure our PrintWriter
+ FileOutputStream fos = new FileOutputStream(fileNew);
+ OutputStreamWriter osw = new OutputStreamWriter(fos);
+ writer = new PrintWriter(osw);
+
+ // Print the file prolog
+ writer.println("<?xml version='1.0'?>");
+ writer.println("<database>");
+
+ // Print entries for each defined user and associated subscriptions
+ User[] yusers = findUsers();
+ for (User yuser : yusers) {
+ writer.print(" ");
+ writer.println(yuser);
+ Subscription[] subscriptions =
+ yuser.getSubscriptions();
+ for (Subscription subscription : subscriptions) {
+ writer.print(" ");
+ writer.println(subscription);
+ writer.print(" ");
+ writer.println("</subscription>");
+ }
+ writer.print(" ");
+ writer.println("</user>");
+ }
+
+ // Print the file epilog
+ writer.println("</database>");
+
+ // Check for errors that occurred while printing
+ if (writer.checkError()) {
+ writer.close();
+ fileNew.delete();
+ throw new IOException("Saving database to '" + pathname + "'");
+ }
+ writer.close();
+ writer = null;
+ } catch (IOException e) {
+ if (writer != null) {
+ writer.close();
+ }
+ fileNew.delete();
+ throw e;
+ }
+
+ // Perform the required renames to permanently save this file
+ File fileOrig = new File(pathname);
+ File fileOld = new File(pathnameOld);
+ if (fileOrig.exists()) {
+ fileOld.delete();
+ if (!fileOrig.renameTo(fileOld)) {
+ throw new IOException
+ ("Renaming '" + pathname + "' to '" + pathnameOld + "'");
+ }
+ }
+ if (!fileNew.renameTo(fileOrig)) {
+ if (fileOld.exists()) {
+ fileOld.renameTo(fileOrig);
+ }
+ throw new IOException
+ ("Renaming '" + pathnameNew + "' to '" + pathname + "'");
+ }
+ fileOld.delete();
+ }
+
+ public boolean isOpen() {
+ return this.open;
+ }
+}
+
+/**
+ * Digester object creation factory for subscription instances.
+ */
+class MemorySubscriptionCreationFactory implements ObjectCreationFactory {
+
+ private Digester digester = null;
+
+ public Digester getDigester() {
+ return (this.digester);
+ }
+
+ public void setDigester(Digester digester) {
+ this.digester = digester;
+ }
+
+ public Object createObject(Attributes attributes) {
+ String host = attributes.getValue("host");
+ User user = (User) digester.peek();
+ Subscription subscription = user.createSubscription(host);
+ String autoConnect = attributes.getValue("autoConnect");
+ if (autoConnect == null) {
+ autoConnect = "false";
+ }
+ if ("true".equalsIgnoreCase(autoConnect) ||
+ "yes".equalsIgnoreCase(autoConnect)) {
+ subscription.setAutoConnect(true);
+ } else {
+ subscription.setAutoConnect(false);
+ }
+ subscription.setPassword(attributes.getValue("password"));
+ subscription.setType(attributes.getValue("type"));
+ subscription.setUsername(attributes.getValue("username"));
+ return (subscription);
+ }
+}
+
+/**
+ * Digester object creation factory for user instances.
+ */
+class MemoryUserCreationFactory implements ObjectCreationFactory {
+
+ public MemoryUserCreationFactory(MemoryUserDatabase database) {
+ this.database = database;
+ }
+
+ private MemoryUserDatabase database = null;
+
+ private Digester digester = null;
+
+ public Digester getDigester() {
+ return (this.digester);
+ }
+
+ public void setDigester(Digester digester) {
+ this.digester = digester;
+ }
+
+ public Object createObject(Attributes attributes) {
+ String username = attributes.getValue("username");
+ User user = database.createUser(username);
+ user.setFromAddress(attributes.getValue("fromAddress"));
+ user.setFullName(attributes.getValue("fullName"));
+ user.setPassword(attributes.getValue("password"));
+ user.setReplyToAddress(attributes.getValue("replyToAddress"));
+ return (user);
+ }
+
+}
diff --git a/mailreader2/src/main/resources/LICENSE.txt b/mailreader2/src/main/resources/LICENSE.txt
new file mode 100644
index 0000000..dd5b3a5
--- /dev/null
+++ b/mailreader2/src/main/resources/LICENSE.txt
@@ -0,0 +1,174 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
diff --git a/mailreader2/src/main/resources/NOTICE.txt b/mailreader2/src/main/resources/NOTICE.txt
new file mode 100644
index 0000000..c9bc884
--- /dev/null
+++ b/mailreader2/src/main/resources/NOTICE.txt
@@ -0,0 +1,5 @@
+Apache Struts
+Copyright 2000-2015 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
\ No newline at end of file
diff --git a/mailreader2/src/main/resources/alternate.properties b/mailreader2/src/main/resources/alternate.properties
new file mode 100644
index 0000000..03dbf27
--- /dev/null
+++ b/mailreader2/src/main/resources/alternate.properties
@@ -0,0 +1,3 @@
+password=Enter your Password here ==>
+struts.logo.path=struts-power.gif
+struts.logo.alt=Powered by Struts
diff --git a/mailreader2/src/main/resources/alternate_ja.properties b/mailreader2/src/main/resources/alternate_ja.properties
new file mode 100644
index 0000000..981adc8
--- /dev/null
+++ b/mailreader2/src/main/resources/alternate_ja.properties
@@ -0,0 +1 @@
+.password=\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b==>
diff --git a/mailreader2/src/main/resources/log4j2.xml b/mailreader2/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..913b299
--- /dev/null
+++ b/mailreader2/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration>
+ <Appenders>
+ <Console name="STDOUT" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Logger name="com.opensymphony.xwork2" level="info"/>
+ <Logger name="org.apache.struts2" level="info"/>
+ <Logger name="org.springframework" level="info"/>
+ <Root level="info">
+ <AppenderRef ref="STDOUT"/>
+ </Root>
+ </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/mailreader2/src/main/resources/mailreader-default.xml b/mailreader2/src/main/resources/mailreader-default.xml
new file mode 100644
index 0000000..ccc1ced
--- /dev/null
+++ b/mailreader2/src/main/resources/mailreader-default.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
+ "http://struts.apache.org/dtds/struts-2.5.dtd">
+
+<struts>
+
+ <package name="mailreader-default" namespace="/" extends="struts-default">
+
+ <interceptors>
+
+ <interceptor name="authentication"
+ class="org.apache.struts.examples.mailreader2.AuthenticationInterceptor"/>
+
+ <interceptor-stack name="user" >
+ <interceptor-ref name="authentication" />
+ <interceptor-ref name="defaultStack"/>
+ </interceptor-stack>
+
+ <interceptor-stack name="user-submit" >
+ <interceptor-ref name="tokenSession" />
+ <interceptor-ref name="user"/>
+ </interceptor-stack>
+
+ <interceptor-stack name="guest" >
+ <interceptor-ref name="defaultStack"/>
+ </interceptor-stack>
+
+ </interceptors>
+
+ <default-interceptor-ref name="user"/>
+
+ <global-results>
+ <result name="error">/pages/Error.jsp</result>
+ <result name="invalid.token">/pages/Error.jsp</result>
+ <result name="login" type="redirectAction">Login_input</result>
+ </global-results>
+
+ <global-exception-mappings>
+ <exception-mapping
+ result="error"
+ exception="java.lang.Throwable"/>
+ </global-exception-mappings>
+
+ </package>
+
+</struts>
diff --git a/mailreader2/src/main/resources/mailreader-support.xml b/mailreader2/src/main/resources/mailreader-support.xml
new file mode 100644
index 0000000..284f85f
--- /dev/null
+++ b/mailreader2/src/main/resources/mailreader-support.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
+ "http://struts.apache.org/dtds/struts-2.5.dtd">
+
+<struts>
+ <package name="mailreader-support" namespace="/" extends="mailreader-default">
+
+ <action name="Tour">
+ <result>/tour.html</result>
+ <interceptor-ref name="guest"/>
+ </action>
+
+ <action name="Welcome" class="org.apache.struts.examples.mailreader2.WelcomeAction">
+ <result>WEB-INF/jsp/Welcome.jsp</result>
+ <interceptor-ref name="guest"/>
+ </action>
+
+ <action name="Logout" class="org.apache.struts.examples.mailreader2.LogoutAction">
+ <result type="redirectAction">Welcome</result>
+ </action>
+
+ <action name="Login_*" method="{1}" class="org.apache.struts.examples.mailreader2.LoginAction">
+ <result name="input">/WEB-INF/jsp/Login.jsp</result>
+ <result name="cancel" type="redirectAction">Welcome</result>
+ <result type="redirectAction">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>
+
+ <action name="Registration_*" method="{1}" class="org.apache.struts.examples.mailreader2.RegistrationAction">
+ <result name="input">/WEB-INF/jsp/Registration.jsp</result>
+ <result type="redirectAction">MainMenu</result>
+ <interceptor-ref name="guest"/>
+ </action>
+ </package>
+
+ <package name="subscription" namespace="/" extends="mailreader-support">
+
+ <global-results>
+ <result name="input">/WEB-INF/jsp/Subscription.jsp</result>
+ <result type="redirectAction">Registration_input</result>
+ </global-results>
+
+ <action name="Subscription_save" method="save" class="org.apache.struts.examples.mailreader2.SubscriptionAction">
+ <interceptor-ref name="user-submit" />
+ </action>
+
+ <action name="Subscription_*" method="{1}" class="org.apache.struts.examples.mailreader2.SubscriptionAction" />
+
+ </package>
+
+ <package name="wildcard" namespace="/" extends="mailreader-support">
+
+ <action name="*" class="org.apache.struts.examples.mailreader2.MailreaderSupport">
+ <result>/WEB-INF/jsp/{1}.jsp</result>
+ </action>
+
+ </package>
+</struts>
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Login-validation.xml b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Login-validation.xml
new file mode 100644
index 0000000..1a65930
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Login-validation.xml
@@ -0,0 +1,14 @@
+<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.2//EN" "http://struts.apache.org/dtds/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>
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport.properties b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport.properties
new file mode 100644
index 0000000..4e79b02
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport.properties
@@ -0,0 +1,97 @@
+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
+button.logon=Log on
+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 login 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=</ul><hr>
+errors.header=<h3><font color="red">Validation Error</font></h3><p>You must correct the following error(s) before proceeding:</p><ul>
+errors.prefix=<li>
+errors.suffix=</li>
+errors.ioException=I/O exception rendering error messages: {0}
+expired.password=User Password has expired for {0}
+heading.autoConnect=Auto
+heading.subscriptions=Current Subscriptions
+heading.host=Host Name
+heading.user=User Name
+heading.type=Server Type
+heading.action=Action
+index.heading=MailReader Demonstration Application Options
+index.login=Log on to the MailReader Demonstration Application
+index.registration=Register with the MailReader Demonstration Application
+index.title=MailReader Demonstration Application
+index.tour=A Walking Tour of the MailReader Demonstration Application
+linkSubscription.io=I/O Error: {0}
+linkSubscription.noSubscription=No subscription under attribute {0}
+linkUser.io=I/O Error: {0}
+linkUser.noUser=No user under attribute {0}
+login.title=MailReader Demonstration Application - Login
+mainMenu.heading=Main Menu Options for
+mainMenu.logout=Log off MailReader Demonstration Application
+mainMenu.registration=Edit your user registration profile
+mainMenu.title=MailReader Demonstration Application - Main Menu
+option.imap=IMAP Protocol
+option.pop3=POP3 Protocol
+# prompt.
+host=Mail Server
+password=Password
+password2=(Repeat) Password
+username=Username
+
+registration.addSubscription=Add
+registration.deleteSubscription=Delete
+registration.editSubscription=Edit
+registration.title.create=Register for the MailReader Demonstration Application
+registration.title.edit=Edit Registration for the MailReader Demonstration Application
+
+subscription.autoConnect=Auto Connect
+subscription.password=Mail Password
+subscription.type=Server Type
+subscription.username=Mail Username
+subscription.title.create=Create New Mail Subscription
+subscription.title.delete=Delete Existing Mail Subscription
+subscription.title.edit=Edit Existing Mail Subscription
+
+user.fromAddress=From Address
+user.fullName=Full Name
+user.replyToAddress=Reply To Address
+
+# Standard error messages for validator framework checks
+errors.required=${getText(fieldName)} is required.
+errors.minlength=${getText(fieldName)} cannot be less than {1} characters.
+errors.maxlength=${getText(fieldName)} cannot be greater than {1} characters.
+errors.invalid=${getText(fieldName)} is invalid.
+errors.byte=${getText(fieldName)} must be an byte.
+errors.short=${getText(fieldName)} must be an short.
+errors.integer=${getText(fieldName)} must be an integer.
+errors.long=${getText(fieldName)} must be an long.
+errors.float=${getText(fieldName)} must be an float.
+errors.double=${getText(fieldName)} must be an double.
+errors.date=${getText(fieldName)} is not a date.
+errors.range=${getText(fieldName)} is not in the range ${minLength} through ${maxLength}.
+errors.creditcard=${getText(fieldName)} is not a valid credit card number.
+errors.email=${getText(fieldName)} is an invalid e-mail address.
+errors.literal=${getText(fieldName)}
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport_ja.properties b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport_ja.properties
new file mode 100644
index 0000000..2b9d2e4
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport_ja.properties
@@ -0,0 +1,89 @@
+button.cancel=\u30ad\u30e3\u30f3\u30bb\u30eb
+button.confirm=\u78ba\u8a8d
+button.reset=\u30ea\u30bb\u30c3\u30c8
+button.save=\u4fdd\u5b58
+change.message=\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u904E\u304E\u307E\u3057\u305F\u3002\u30B7\u30B9\u30C6\u30E0\u7BA1\u7406\u8005\u306B\u304A\u554F\u3044\u5408\u308F\u305B\u4E0B\u3055\u3044
+change.try=\u518D\u8A66\u884C
+change.title=\u30d1\u30b9\u30ef\u30fc\u30c9\u671f\u9650\u5207\u308c
+database.load= {0} \u304B\u3089\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3092\u30ED\u30FC\u30C9\u3067\u304D\u307E\u305B\u3093
+error.database.missing=\u30E6\u30FC\u30B6\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u30ED\u30B0\u30AA\u30F3\u306E\u8A8D\u8A3C\u304C\u51FA\u6765\u307E\u305B\u3093
+error.fromAddress.format=From\u30A2\u30C9\u30EC\u30B9\u306E\u66F8\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093
+error.fromAddress.required=From\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
+error.fullName.required=\u30D5\u30EB\u30CD\u30FC\u30E0\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
+error.host.required=\u30E1\u30FC\u30EB\u30B5\u30FC\u30D0\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
+error.noSubscription=Subscription bean \u304c\u30bb\u30c3\u30b7\u30e7\u30f3\u306e\u4e2d\u306b\u3042\u308a\u307e\u305b\u3093
+error.password.expired=\u30E6\u30FC\u30B6 {0} \u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u904E\u304E\u307E\u3057\u305F
+error.password.required=\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u5FC5\u8981\u3067\u3059
+error.password2.required=\u30D1\u30B9\u30EF\u30FC\u30C9(\u78BA\u8A8D\u7528)\u304C\u5FC5\u8981\u3067\u3059
+error.password.match=\u30D1\u30B9\u30EF\u30FC\u30C9\u3068\u78BA\u8A8D\u7528\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u4E00\u81F4\u3057\u3066\u3044\u307E\u305B\u3093
+error.password.mismatch=\u30E6\u30FC\u30B6\u540D\u307E\u305F\u306F\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u4E0D\u6B63\u3067\u3059\u3002\u518D\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
+error.replyToAddress.format=\u8FD4\u4FE1\u30A2\u30C9\u30EC\u30B9\u306E\u66F8\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093
+struts.messages.invalid.token=\u3053\u306E\u30D5\u30A9\u30FC\u30E0\u306E\u5185\u5BB9\u304C\u6B63\u3057\u304F\u306A\u3044\u305F\u3081\u9001\u4FE1\u3059\u308B\u3053\u3068\u304C\u51FA\u6765\u307E\u305B\u3093
+error.type.invalid=\u30B5\u30FC\u30D0\u30BF\u30A4\u30D7\u306F 'imap' \u304B 'pop3'\u306E\u3069\u3061\u3089\u304B\u3067\u306A\u3051\u308C\u3070\u306A\u308A\u307E\u305B\u3093
+error.type.required=\u30B5\u30FC\u30D0\u30BF\u30A4\u30D7\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
+error.username.required=\u30E6\u30FC\u30B6\u540D\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
+error.username.unique=\u305D\u306E\u30E6\u30FC\u30B6\u540D\u306F\u65E2\u306B\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059\u3002 \u5225\u306E\u30E6\u30FC\u30B6\u540D\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044
+errors.footer=</ul><hr>
+errors.header=<h3><font color="red">\u5165\u529b\u30c1\u30a7\u30c3\u30af\u30a8\u30e9\u30fc</font></h3><p>\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044:</p><ul>
+errors.prefix=<li>
+errors.suffix=</li>
+errors.ioException=I/O\u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {0}
+expired.password=\u30E6\u30FC\u30B6 {0} \u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u904E\u304E\u307E\u3057\u305F
+heading.autoConnect=\u81ea\u52d5\u63a5\u7d9a
+heading.subscriptions=\u73fe\u5728\u306e\u8cfc\u8aad\u60c5\u5831
+heading.host=\u30db\u30b9\u30c8\u540d
+heading.user=\u30e6\u30fc\u30b6\u540d
+heading.type=\u30b5\u30fc\u30d0\u30bf\u30a4\u30d7
+heading.action=\u64cd\u4f5c
+index.heading=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 \u30aa\u30d7\u30b7\u30e7\u30f3
+index.login=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30ed\u30b0\u30aa\u30f3
+index.registration=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30e6\u30fc\u30b6\u767b\u9332
+index.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3(Struts 1.1-dev)
+index.tour=\u30b5\u30f3\u30d7\u30eb\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u6563\u7b56\u3059\u308b
+linkSubscription.io=I/O\u30a8\u30e9\u30fc: {0}
+linkSubscription.noSubscription=\u5c5e\u6027 {0} \u306b\u8cfc\u8aad\u60c5\u5831\u304c\u5b58\u5728\u3057\u307e\u305b\u3093
+linkUser.io=I/O\u30a8\u30e9\u30fc: {0}
+linkUser.noUser=\u5c5e\u6027 {0} \u306b\u30e6\u30fc\u30b6\u60c5\u5831\u304c\u5b58\u5728\u3057\u307e\u305b\u3093
+login.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30ed\u30b0\u30aa\u30f3
+mainMenu.heading=\u30e1\u30a4\u30f3\u30e1\u30cb\u30e5\u30fc
+mainMenu.logout=MailReader \u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30ed\u30b0\u30aa\u30d5
+mainMenu.registration=\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u7de8\u96c6
+mainMenu.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30e1\u30a4\u30f3\u30e1\u30cb\u30e5\u30fc
+option.imap=IMAP \u30d7\u30ed\u30c8\u30b3\u30eb
+option.pop3=POP3 \u30d7\u30ed\u30c8\u30b3\u30eb
+# prompt.
+autoConnect=\u81ea\u52d5\u63a5\u7d9a
+fromAddress=From\u30a2\u30c9\u30ec\u30b9
+fullName=\u30d5\u30eb\u30cd\u30fc\u30e0
+mailHostname=\u30e1\u30fc\u30eb\u30b5\u30fc\u30d0
+mailPassword=\u30e1\u30fc\u30eb\u30d1\u30b9\u30ef\u30fc\u30c9
+mailServerType=\u30b5\u30fc\u30d0\u30bf\u30a4\u30d7
+mailUsername=\u30e1\u30fc\u30eb\u30e6\u30fc\u30b6\u540d
+.password=\u30d1\u30b9\u30ef\u30fc\u30c9
+password2=\u30d1\u30b9\u30ef\u30fc\u30c9(\u78ba\u8a8d\u7528)
+replyToAddress=\u8fd4\u4fe1\u30a2\u30c9\u30ec\u30b9
+username=\u30e6\u30fc\u30b6\u540d
+registration.addSubscription=\u65b0\u898f\u4f5c\u6210
+registration.deleteSubscription=\u524a\u9664
+registration.editSubscription=\u7de8\u96c6
+registration.title.create=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30e6\u30fc\u30b6\u767b\u9332
+registration.title.edit=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u7de8\u96c6
+subscription.title.create=\u30e1\u30fc\u30eb\u8cfc\u8aad\u60c5\u5831\u306e\u65b0\u898f\u4f5c\u6210
+subscription.title.delete=\u30e1\u30fc\u30eb\u8cfc\u8aad\u60c5\u5831\u306e\u524a\u9664
+subscription.title.edit=\u30e1\u30fc\u30eb\u8cfc\u8aad\u60c5\u5831\u306e\u7de8\u96c6
+
+# Standard error messages for validator framework checks
+errors.required=${getText(fieldName)} \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+errors.minlength=${getText(fieldName)} \u306f {1} \u6587\u5b57\u4ee5\u4e0a\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.maxlength=${getText(fieldName)} \u306f {2} \u6587\u5b57\u4ee5\u4e0b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.invalid=${getText(fieldName)} \u306f\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002
+errors.byte=${getText(fieldName)} \u306fbyte\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.short=${getText(fieldName)} \u306fshort\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.integer=${getText(fieldName)} \u306finteger\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.long=${getText(fieldName)} \u306flong\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.float=${getText(fieldName)} \u306ffloat\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.double=${getText(fieldName)} \u306fdouble\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.date=${getText(fieldName)} \u306f\u65e5\u4ed8\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+errors.range=${getText(fieldName)} \u306f {1} \u304b\u3089 {2} \u306e\u9593\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+errors.creditcard=${getText(fieldName)} \u306f\u6b63\u3057\u3044\u30af\u30ec\u30b8\u30c3\u30c8\u30ab\u30fc\u30c9\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+errors.email=${getText(fieldName)} \u306f\u6b63\u3057\u3044\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport_ru.properties b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport_ru.properties
new file mode 100644
index 0000000..6c7a88b
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/MailreaderSupport_ru.properties
@@ -0,0 +1,89 @@
+button.cancel=\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c
+button.confirm=\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c
+button.reset=\u0421\u0431\u0440\u043e\u0441\u0438\u0442\u044c
+button.save=\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c
+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=\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u0437 {0}
+error.database.missing=\u041d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f - \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e.
+error.fromAddress.format=\u0412 \u043f\u043e\u043b\u0435 '\u0410\u0434\u0440\u0435\u0441 \u041e\u0442:' \u0443\u043a\u0430\u0437\u0430\u043d \u0430\u0434\u0440\u0435\u0441 \u0432 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435.
+error.fromAddress.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0432 \u043f\u043e\u043b\u0435 '\u0410\u0434\u0440\u0435\u0441 \u041e\u0442:'.
+error.fullName.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u043b\u043d\u043e\u0435 \u0438\u043c\u044f.
+error.host.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440.
+error.noSubscription=\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0432 \u0441\u0435\u0441\u0441\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f
+error.password.expired=Your password has expired for username {0}
+error.password.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c.
+error.password2.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f.
+error.password.match=\u041f\u0430\u0440\u043e\u043b\u044c \u0438 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f \u043d\u0435 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.
+error.password.mismatch=\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0435 \u0438\u043c\u044f \u0438/\u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c - \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0441\u043d\u043e\u0432\u0430.
+error.replyToAddress.format=\u0412 \u043f\u043e\u043b\u0435 '\u0410\u0434\u0440\u0435\u0441 \u041e\u0442\u0432\u0435\u0442\u0438\u0442\u044c \u043d\u0430:' \u0443\u043a\u0430\u0437\u0430\u043d \u0430\u0434\u0440\u0435\u0441 \u0432 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435.
+struts.messages.invalid.token=\u042d\u0442\u0430 \u0444\u043e\u0440\u043c\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u0430 - \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u043f\u043e\u0440\u044f\u0434\u043a\u0430 \u0437\u0430\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445.
+error.type.invalid=\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0438\u043f\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043b\u0438\u0448\u044c 'imap' \u0438\u043b\u0438 'pop3'
+error.type.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u0438\u043f \u0441\u0435\u0440\u0432\u0435\u0440\u0430
+error.username.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f
+error.username.unique=\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f - \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0434\u0440\u0443\u0433\u043e\u0435 \u0438\u043c\u044f.
+errors.footer=</ul><hr>
+errors.header=<h3><font color="red">\u041e\u0448\u0438\u0431\u043a\u0438 \u043f\u0440\u0438 \u0437\u0430\u043d\u0435\u0441\u0435\u043d\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445</font></h3><p>\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:</p><ul>
+errors.prefix=<li>
+errors.suffix=</li>
+errors.ioException=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0430\u0445: {0}
+expired.password=User Password has expired for {0}
+heading.autoConnect=\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438
+heading.subscriptions=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438
+heading.host=\u0421\u0435\u0440\u0432\u0435\u0440
+heading.user=\u0418\u043c\u044f
+heading.type=\u0422\u0438\u043f \u0441\u0435\u0440\u0432\u0435\u0440\u0430
+heading.action=\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435
+index.heading=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b'
+index.login=\u0412\u043e\u0439\u0442\u0438 \u043a\u0430\u043a \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c
+index.registration=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f
+index.title=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b' (Struts 1.1-dev)
+index.tour=\u041e\u0431\u0437\u043e\u0440 \u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b'
+linkSubscription.io=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 (\u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438): {0}
+linkSubscription.noSubscription=\u0410\u0442\u0440\u0438\u0431\u0443\u0442 {0} \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0435 \u0438\u043b\u0438 \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.
+linkUser.io=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 (\u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f): {0}
+linkUser.noUser=\u0410\u0442\u0440\u0438\u0431\u0443\u0442 {0} \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435 \u0438\u043b\u0438 \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.
+login.title=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b - \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0438\u043c\u0435\u043d\u0438 \u0438 \u043f\u0430\u0440\u043e\u043b\u044f.
+mainMenu.heading=\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0433\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043c\u0435\u043d\u044e \u0434\u043b\u044f
+mainMenu.logout=\u0412\u044b\u0439\u0442\u0438
+mainMenu.registration=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u0432\u043e\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
+mainMenu.title=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b' - \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435 \u043c\u0435\u043d\u044e
+option.imap=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b IMAP
+option.pop3=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b POP3
+# prompt.
+autoConnect=\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435:
+fromAddress=\u0410\u0434\u0440\u0435\u0441 \u041e\u0442:
+fullName=\u041f\u043e\u043b\u043d\u043e\u0435 \u0438\u043c\u044f:
+mailHostname=\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440:
+mailPassword=\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430:
+mailServerType=\u0422\u0438\u043f \u0441\u0435\u0440\u0432\u0435\u0440\u0430:
+mailUsername=\u0418\u043c\u044f \u0434\u043b\u044f \u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430:
+password=\u041f\u0430\u0440\u043e\u043b\u044c:
+password2=(\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435) \u041f\u0430\u0440\u043e\u043b\u044c:
+replyToAddress=\u0410\u0434\u0440\u0435\u0441 \u041e\u0442\u0432\u0435\u0442\u0438\u0442\u044c \u043d\u0430:
+username=\u0418\u043c\u044f:
+registration.addSubscription=\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c
+registration.deleteSubscription=\u0423\u0434\u0430\u043b\u0438\u0442\u044c
+registration.editSubscription=\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c
+registration.title.create=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f
+registration.title.edit=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0441\u0432\u043e\u0435\u0439 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438
+subscription.title.create=\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443
+subscription.title.delete=\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443
+subscription.title.edit=\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443
+
+# Standard error messages for validator framework checks
+errors.required=${getText(fieldName)} is required.
+errors.minlength=${getText(fieldName)} cannot be less than {1} characters.
+errors.maxlength=${getText(fieldName)} cannot be greater than {2} characters.
+errors.invalid=${getText(fieldName)} is invalid.
+errors.byte=${getText(fieldName)} must be an byte.
+errors.short=${getText(fieldName)} must be an short.
+errors.integer=${getText(fieldName)} must be an integer.
+errors.long=${getText(fieldName)} must be an long.
+errors.float=${getText(fieldName)} must be an float.
+errors.double=${getText(fieldName)} must be an double.
+errors.date=${getText(fieldName)} is not a date.
+errors.range=${getText(fieldName)} is not in the range {1} through {2}.
+errors.creditcard=${getText(fieldName)} is not a valid credit card number.
+errors.email=${getText(fieldName)} is an invalid e-mail address.
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Registration-Registration_save-validation.xml b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Registration-Registration_save-validation.xml
new file mode 100644
index 0000000..509ca5f
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Registration-Registration_save-validation.xml
@@ -0,0 +1,28 @@
+<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.2//EN" "http://struts.apache.org/dtds/xwork-validator-1.0.2.dtd">
+
+<validators>
+
+ <field name="password">
+ <field-validator type="requiredstring">
+ <message key="error.password.required"/>
+ </field-validator>
+ <field-validator type="stringlength">
+ <param name="trim">true</param>
+ <param name="minLength">4</param>
+ <param name="maxLength">10</param>
+ <message key="errors.range"/>
+ </field-validator>
+ </field>
+
+ <field name="password2">
+ <field-validator type="requiredstring">
+ <message key="error.password2.required"/>
+ </field-validator>
+ </field>
+
+ <validator type="expression">
+ <param name="expression">password eq password2</param>
+ <message key="error.password.match"/>
+ </validator>
+
+</validators>
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Registration-validation.xml b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Registration-validation.xml
new file mode 100644
index 0000000..a123125
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Registration-validation.xml
@@ -0,0 +1,32 @@
+<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.2//EN" "http://struts.apache.org/dtds/xwork-validator-1.0.2.dtd">
+
+<validators>
+
+ <field name="username">
+ <field-validator type="requiredstring">
+ <message key="error.username.required"/>
+ </field-validator>
+ </field>
+
+ <field name="user.fullName">
+ <field-validator type="requiredstring">
+ <message key="error.fullName.required"/>
+ </field-validator>
+ </field>
+
+ <field name="user.fromAddress">
+ <field-validator type="requiredstring">
+ <message key="error.fromAddress.required"/>
+ </field-validator>
+ <field-validator type="email">
+ <message key="errors.email"/>
+ </field-validator>
+ </field>
+
+ <field name="user.replyToAddress">
+ <field-validator type="email">
+ <message key="errors.email"/>
+ </field-validator>
+ </field>
+
+</validators>
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Subscription-Subscription_save-validation.xml b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Subscription-Subscription_save-validation.xml
new file mode 100644
index 0000000..177bf6c
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Subscription-Subscription_save-validation.xml
@@ -0,0 +1,23 @@
+<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.2//EN" "http://struts.apache.org/dtds/xwork-validator-1.0.2.dtd">
+
+<validators>
+
+ <field name="subscription.username">
+ <field-validator type="requiredstring">
+ <message key="error.username.required"/>
+ </field-validator>
+ </field>
+
+ <field name="subscription.password">
+ <field-validator type="requiredstring">
+ <message key="error.password.required"/>
+ </field-validator>
+ </field>
+
+ <field name="subscription.type">
+ <field-validator type="requiredstring">
+ <message key="error.type.invalid"/>
+ </field-validator>
+ </field>
+
+</validators>
diff --git a/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Subscription-validation.xml b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Subscription-validation.xml
new file mode 100644
index 0000000..917ef2a
--- /dev/null
+++ b/mailreader2/src/main/resources/org/apache/struts/examples/mailreader2/Subscription-validation.xml
@@ -0,0 +1,11 @@
+<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.2//EN" "http://struts.apache.org/dtds/xwork-validator-1.0.2.dtd">
+
+<validators>
+
+ <field name="host">
+ <field-validator type="requiredstring">
+ <message key="error.host.required"/>
+ </field-validator>
+ </field>
+
+</validators>
diff --git a/mailreader2/src/main/resources/struts.xml b/mailreader2/src/main/resources/struts.xml
new file mode 100644
index 0000000..37182fe
--- /dev/null
+++ b/mailreader2/src/main/resources/struts.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
+ "http://struts.apache.org/dtds/struts-2.5.dtd">
+
+<struts>
+
+ <constant name="struts.action.extension" value="do" />
+ <constant name="struts.devMode" value="true" />
+ <constant name="struts.enable.DynamicMethodInvocation" value="false" />
+
+ <include file="mailreader-default.xml"/>
+
+ <include file="mailreader-support.xml"/>
+
+</struts>
\ No newline at end of file
diff --git a/mailreader2/src/main/resources/velocity.properties b/mailreader2/src/main/resources/velocity.properties
new file mode 100644
index 0000000..6299831
--- /dev/null
+++ b/mailreader2/src/main/resources/velocity.properties
@@ -0,0 +1 @@
+runtime.log.logsystem.class=org.apache.velocity.runtime.log.NullLogChute
diff --git a/mailreader2/src/main/webapp/META-INF/context.xml b/mailreader2/src/main/webapp/META-INF/context.xml
new file mode 100644
index 0000000..9b0b5d0
--- /dev/null
+++ b/mailreader2/src/main/webapp/META-INF/context.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Context path="/">
+</Context>
diff --git a/mailreader2/src/main/webapp/WEB-INF/database.xml b/mailreader2/src/main/webapp/WEB-INF/database.xml
new file mode 100644
index 0000000..e54d9c5
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/database.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0'?>
+<database>
+ <user username="user" fromAddress="John.User@somewhere.com" fullName="John Q. User" password="pass">
+ <subscription host="mail.yahoo.com" autoConnect="false" password="foo" type="imap" username="jquser">
+ </subscription>
+ <subscription host="mail.hotmail.com" autoConnect="false" password="bar" type="pop3" username="user1234">
+ </subscription>
+ </user>
+</database>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/ChangePassword.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/ChangePassword.jsp
new file mode 100644
index 0000000..ce543d7
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/ChangePassword.jsp
@@ -0,0 +1,25 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!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="change.title"/></title>
+ <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+</head>
+
+<body>
+
+<p>
+ <s:text name="change.message"/>
+</p>
+
+<p>
+ <a href="<s:url action="Login_input"/>">
+ <s:text name="change.try"/>
+ </a>
+</p>
+
+</body>
+</html>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/Error.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/Error.jsp
new file mode 100644
index 0000000..05ab5e0
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/Error.jsp
@@ -0,0 +1,40 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!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>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/Footer.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/Footer.jsp
new file mode 100644
index 0000000..56d80bb
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/Footer.jsp
@@ -0,0 +1,6 @@
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<hr/>
+
+<p>
+ <a href="<s:url action="Welcome" includeParams="none"/>"><s:text name="index.title"/></a>
+</p>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/Login.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/Login.jsp
new file mode 100644
index 0000000..2994793
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/Login.jsp
@@ -0,0 +1,30 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <title><s:text name="login.title"/></title>
+ <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+</head>
+
+<body onLoad="self.focus();document.Login.username.focus()">
+
+<s:actionerror />
+<s:form action="Login" validate="true">
+ <s:textfield key="username" />
+
+ <s:password key="password" showPassword="true"/>
+
+ <s:submit key="button.logon"/>
+
+ <s:reset key="button.reset"/>
+
+ <s:submit action="Login_cancel" key="button.cancel"
+ onclick="form.onsubmit=null"/>
+</s:form>
+
+<jsp:include page="Footer.jsp"/>
+</body>
+</html>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/MainMenu.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/MainMenu.jsp
new file mode 100644
index 0000000..bf2f044
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/MainMenu.jsp
@@ -0,0 +1,25 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!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="Logout"/>">
+ <s:text name="mainMenu.logout"/>
+ </a>
+</ul>
+</body>
+</html>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/Registration.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/Registration.jsp
new file mode 100644
index 0000000..1d3cf8d
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/Registration.jsp
@@ -0,0 +1,115 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!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="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>
+
+<body onLoad="self.focus();document.Registration_save_username.focus()">
+
+<s:actionerror/>
+<s:form action="Registration_save" validate="false">
+ <s:token />
+ <s:hidden name="task"/>
+ <s:if test="task == 'Create'">
+ <s:textfield key="username"/>
+ </s:if>
+ <s:else>
+ <s:label key="username"/>
+ <s:hidden name="username"/>
+ </s:else>
+
+ <s:password key="password" showPassword="true"/>
+ <s:password key="password2"/>
+ <s:textfield key="user.fullName"/>
+ <s:textfield key="user.fromAddress"/>
+ <s:textfield key="user.replyToAddress"/>
+
+ <s:if test="task == 'Create'">
+ <s:submit key="button.save" action="Registration_save"/>
+ <s:reset key="button.reset"/>
+ <s:submit action="Welcome" key="button.cancel"
+ onclick="form.onsubmit=null"/>
+ </s:if>
+ <s:else>
+ <s:submit key="button.save" action="Registration"/>
+ <s:reset key="button.reset"/>
+ <s:submit action="MainMenu" key="button.cancel"
+ onclick="form.onsubmit=null"/>
+ </s:else>
+
+</s:form>
+
+<s:if test="task == 'Edit'">
+ <div align="center">
+ <h3><s:text name="heading.subscriptions"/></h3>
+ </div>
+
+ <table border="1" width="100%">
+
+ <tr>
+ <th align="center" width="30%">
+ <s:text name="heading.host"/>
+ </th>
+ <th align="center" width="25%">
+ <s:text name="heading.user"/>
+ </th>
+ <th align="center" width="10%">
+ <s:text name="heading.type"/>
+ </th>
+ <th align="center" width="10%">
+ <s:text name="heading.autoConnect"/>
+ </th>
+ <th align="center" width="15%">
+ <s:text name="heading.action"/>
+ </th>
+ </tr>
+
+ <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>
+
+ </table>
+
+ <a href="<s:url action="Subscription_input"/>"><s:text
+ name="registration.addSubscription"/></a>
+
+</s:if>
+
+<jsp:include page="Footer.jsp"/>
+
+</body>
+</html>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/Subscription.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/Subscription.jsp
new file mode 100644
index 0000000..60cda4a
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/Subscription.jsp
@@ -0,0 +1,60 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!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" includeParams="none"/>" rel="stylesheet"
+ type="text/css"/>
+</head>
+
+<body onLoad="self.focus();document.Subscription.username.focus()">
+
+<s:actionerror/>
+<s:form action="Subscription_save" validate="true">
+ <s:token />
+ <s:hidden name="task"/>
+ <s:label key="username" name="user.username"/>
+
+ <s:if test="task == 'Create'">
+ <s:textfield key="host"/>
+ </s:if>
+ <s:else>
+ <s:label key="host"/>
+ <s:hidden name="host"/>
+ </s:else>
+
+ <s:if test="task == 'Delete'">
+ <s:label key="subscription.username"/>
+ <s:label key="subscription.password"/>
+ <s:label key="subscription.type"/>
+ <s:label key="subscription.autoConnect"/>
+ <s:submit key="button.confirm"/>
+ </s:if>
+ <s:else>
+ <s:textfield key="subscription.username"/>
+ <s:textfield key="subscription.password"/>
+ <s:select key="subscription.type" list="types"/>
+ <s:checkbox key="subscription.autoConnect"/>
+ <s:submit key="button.save"/>
+ <s:reset key="button.reset"/>
+ </s:else>
+
+ <s:submit action="Registration_input"
+ key="button.cancel"
+ onclick="form.onsubmit=null"/>
+</s:form>
+
+<jsp:include page="Footer.jsp"/>
+
+</body>
+</html>
diff --git a/mailreader2/src/main/webapp/WEB-INF/jsp/Welcome.jsp b/mailreader2/src/main/webapp/WEB-INF/jsp/Welcome.jsp
new file mode 100644
index 0000000..0ce3e54
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/jsp/Welcome.jsp
@@ -0,0 +1,55 @@
+<%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib uri="/struts-tags" prefix="s" %>
+<!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="Login_input"/>"><s:text
+ name="index.login"/></a></li>
+</ul>
+
+<h3>Language Options</h3>
+<ul>
+ <li>
+ <s:url id="en" action="Welcome">
+ <s:param name="request_locale">en</s:param>
+ </s:url>
+ <s:a href="%{en}">English</s:a>
+ </li>
+ <li>
+ <s:url id="ja" action="Welcome">
+ <s:param name="request_locale">ja</s:param>
+ </s:url>
+ <s:a href="%{ja}">Japanese</s:a>
+ </li>
+ <li>
+ <s:url id="ru" action="Welcome">
+ <s:param name="request_locale">ru</s:param>
+ </s:url>
+ <s:a href="%{ru}">Russian</s:a>
+ </li>
+</ul>
+
+<hr/>
+
+<p><s:i18n name="alternate"><a href="http://struts.apache.org/">
+ <img src="<s:text name="struts.logo.path"/>"
+ alt="<s:text name="struts.logo.alt"/>" border="0px"/>
+</a>
+</s:i18n></p>
+
+</body>
+</html>
+
diff --git a/mailreader2/src/main/webapp/WEB-INF/web.xml b/mailreader2/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..6437955
--- /dev/null
+++ b/mailreader2/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,45 @@
+<?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.filter.StrutsPrepareAndExecuteFilter</filter-class>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>struts2</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+ <!-- Application Listener for Mailreader database -->
+ <listener>
+ <listener-class>
+ org.apache.struts.examples.mailreader2.ApplicationListener
+ </listener-class>
+ </listener>
+
+ <welcome-file-list>
+ <welcome-file>index.html</welcome-file>
+ </welcome-file-list>
+
+ <!-- Restricts access to pure JSP files - access available only via Struts action -->
+ <security-constraint>
+ <display-name>No direct JSP access</display-name>
+ <web-resource-collection>
+ <web-resource-name>No-JSP</web-resource-name>
+ <url-pattern>*.jsp</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>no-users</role-name>
+ </auth-constraint>
+ </security-constraint>
+
+ <security-role>
+ <description>Don't assign users to this role</description>
+ <role-name>no-users</role-name>
+ </security-role>
+
+</web-app>
diff --git a/mailreader2/src/main/webapp/css/mailreader.css b/mailreader2/src/main/webapp/css/mailreader.css
new file mode 100644
index 0000000..bfc7648
--- /dev/null
+++ b/mailreader2/src/main/webapp/css/mailreader.css
@@ -0,0 +1,46 @@
+/**
+* Mailreader stylesheet
+*/
+
+body {
+ background-color: #FFFFFF;
+ color: #000000;
+ link: 000066;
+ visited: #660066;
+ active: #33CCCC;
+}
+
+A:hover {
+ color: #FF0000;
+}
+
+h1 {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+h2 {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+h3 {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+h4 {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+h5 {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+h6 {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+font.hint {
+ font-style: italic;
+ font-size: 80%;
+ font-family: Arial, Helvetica, sans-serif;
+ text-align: left;
+}
\ No newline at end of file
diff --git a/mailreader2/src/main/webapp/index.html b/mailreader2/src/main/webapp/index.html
new file mode 100644
index 0000000..1b01b3f
--- /dev/null
+++ b/mailreader2/src/main/webapp/index.html
@@ -0,0 +1,10 @@
+<!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>
diff --git a/mailreader2/src/main/webapp/struts-power.gif b/mailreader2/src/main/webapp/struts-power.gif
new file mode 100644
index 0000000..5f4e9d4
Binary files /dev/null and b/mailreader2/src/main/webapp/struts-power.gif differ
diff --git a/mailreader2/src/main/webapp/tour.html b/mailreader2/src/main/webapp/tour.html
new file mode 100644
index 0000000..4f5aacf
--- /dev/null
+++ b/mailreader2/src/main/webapp/tour.html
@@ -0,0 +1,2470 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=us-ascii"/>
+ <link rel="stylesheet" type="text/css" href="../css/mailreader.css"/>
+
+ <title>A Walking Tour of the Struts 2 MailReader Application</title>
+</head>
+
+<body>
+<blockquote>
+<h2>A Walking Tour of the Struts 2 MailReader Application</h2>
+
+<p>
+ <i>
+ This article is meant to introduce a new user to Apache Struts 2 by
+ "walking through" a simple, but functional, application.
+ The article includes code snippets, but for the best result, you might
+ want to install the MailReader application on your own development
+ workstation and follow along.
+ Of course, the full source code to the MailReader is included in the
+ distribution.
+ </i>
+</p>
+
+<p>
+ <i>
+ The tour assumes the reader has a basic understanding of the Java
+ language, JavaBeans, web applications, and JavaServer Pages. For
+ background on these technologies, see the
+ <a href="http://struts.apache.org/primer.html">
+ Key Technologies Primer</a>.
+ </i>
+</p>
+
+<hr/>
+
+<ul>
+ <li>
+ <a href="#Welcome">Welcome</a>
+
+ <ul>
+ <li><a href="#web.xml">web.xml and resources.properties</a></li>
+
+ <li><a href="#Welcome.do">Welcome.do</a></li>
+
+ <li><a href="#Welcome.java">Welcome Action</a></li>
+
+ <li><a href="#global-results">Global Results</a></li>
+
+ <li><a href="#ApplicationListener.java">ApplicationListener.java</a></li>
+
+ <li><a href="#resources.properties">Message Resources</a></li>
+
+ <li><a href="#Welcome.jsp">Welcome Page</a></li>
+
+ </ul>
+ </li>
+</ul>
+
+<ul>
+ <li>
+ <a href="#Login">Login</a>
+ <ul>
+
+ <li><a href="#Login.jsp">Login Page</a></li>
+
+ <li><a href="#Login-validation.xml">Login-validation.xml</a></li>
+
+ <li><a href="#Login.java">Login.java</a></li>
+
+ <li><a href="#MailreaderSupport.java">MailreaderSupport.java</a></li>
+
+ <li><a href="#Login.xml">Login Configuration</a></li>
+
+ </ul>
+ </li>
+</ul>
+
+<ul>
+ <li>
+ <a href="#MainMenu">MainMenu</a>
+ </li>
+</ul>
+
+<ul>
+ <li>
+ <a href="#Registration.jsp">Registration page</a>
+ <ul>
+ <li><a href="#iterator">iterator</a></li>
+ </ul>
+ </li>
+</ul>
+
+<ul>
+ <li>
+ <a href="#Subscription">Subscription</a>
+
+ <ul>
+ <li><a href="#Subscription.java">Subscription.java</a>
+ </li>
+ </ul>
+ </li>
+</ul>
+<hr/>
+
+<p>
+ The premise of the MailReader is that it is the first iteration of a
+ portal application.
+ This version allows users to register and maintain a set of
+ accounts with various mail servers.
+ If completed, the application would let users read mail from their
+ accounts.
+</p>
+
+<p>
+ The MailReader application demonstrates registering with an application,
+ logging into an application, maintaining a master record, and maintaining
+ child records.
+ This article overviews the constructs needed to do these things,
+ including the server pages, Java classes, and configuration elements.
+</p>
+
+<p>
+ For more about the MailReader, including alternate implementations and a
+ set of formal Use Cases,
+ please visit the <a href="http://www.StrutsUniversity.org/MailReader">
+ Struts University MailReader site</a>.
+</p>
+
+<hr/>
+<blockquote>
+ <p><font class="hint">
+ <strong>JAAS</strong> -
+ Note that for compatibility and ease of deployment, the MailReader
+ uses "application-based" authorization.
+ However, use of the standard Java Authentication and Authorization
+ Service (JAAS) is recommended for most applications.
+ (See the <a
+ href="http://struts.apache.org/primer.html">
+ Key Technologies Primer</a> for more about
+ authentication technologies.)
+ </font></p>
+</blockquote>
+<hr/>
+
+<p>
+ The tour starts with how the initial welcome page is displayed, and
+ then steps through logging into the application and editing a subscription.
+ Please note that this not a quick peek at a "Hello World" application.
+ The tour is a rich trek into a realistic, best practices application.
+ You may need to adjust your chair and get a fresh cup of coffee.
+ Printed, the article is 29 pages long (US).
+</p>
+
+<h3><a name="Welcome" id="Welcome">Welcome Page</a></h3>
+
+<p>
+ A web application, like any other web site, can specify a list of welcome pages.
+ When you open a web application without specifying a particular page, a
+ default "welcome page" is served as the response.
+</p>
+
+<h4><a name="web.xml" id="web.xml">web.xml</a></h4>
+
+<p>
+ When a web application loads,
+ the container reads and parses the "Web Application Deployment
+ Descriptor", or "web.xml" file.
+ The framework plugs into a web application via a servlet filter.
+ Like any filter, the "struts2" filter is deployed via the "web.xml".
+</p>
+
+<hr/>
+<h5>web.xml - The Web Application Deployment Descriptor</h5>
+<pre><code><?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+ "http://java.sun.com/dtd/web-app_2_3.dtd">
+<web-app>
+
+ <display-name>Struts 2 MailReader</display-name>
+
+ <strong><filter>
+ <filter-name>struts2</filter-name>
+ <filter-class>
+ org.apache.struts2.dispatcher.FilterDispatcher
+ </filter-class>
+ </filter></strong>
+
+ <filter-mapping>
+ <filter-name><strong>struts2</strong></filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+ <listener>
+ <listener-class>
+ org.springframework.web.context.ContextLoaderListener
+ </listener-class>
+ </listener>
+
+ <!-- Application Listener for MailReader database -->
+ <listener>
+ <listener-class>
+ mailreader2.ApplicationListener
+ </listener-class>
+ </listener>
+
+ <welcome-file-list>
+ <welcome-file>index.html</welcome-file>
+ </welcome-file-list>
+
+ </web-app></code></pre>
+<hr/>
+
+<p>
+ You might note that the web.xml configuration does not specify which file extension
+ to use with actions.
+ The default extension for Struts 2 is ".action",
+ but the extension can be changed in the struts.properties file.
+ For compatability with prior releases, the MailReader uses a .do extension for actions.
+</p>
+
+<hr/>
+<h5>struts.properties</h5>
+<pre><code>struts.action.extension = <strong>do</strong></code></pre>
+<hr/>
+
+<p>
+ The web.xml does specify a "Welcome File List" for the application.
+ When a web address refers to a directory rather than an individual file,
+ the container consults the Welcome File List for the name of a page to
+ open by default.
+</p>
+
+<p>
+ However, most Struts applications do not refer to physical pages,
+ but to "virtual resources" called <i>actions</i>.
+ Actions specify code that we want to be run before a page
+ or other resource renders the response.
+ An accepted practice is to never link directly to server pages,
+ but only to logical action mappings.
+ By linking to actions, developers can often "rewire" an application
+ without editing the server pages.
+</p>
+
+<hr/>
+<h5>Best Practice:</h5>
+<blockquote>
+ <p><font class="hint">"Link actions not pages."</font></p>
+</blockquote>
+<hr/>
+
+<p>
+ The actions are listed in one or more XML configuration files,
+ the default configuration file being named "struts.xml".
+ When the application loads, the struts.xml, and any other files
+ it includes, are parsed, and the framework creates a set of
+ configuration objects.
+ Among other things, the configuration maps a request for a certain
+ page to a certain action mapping.
+</p>
+
+
+<p>
+ Sites can list zero or more "Welcome" pages in the web.xml.
+ <a href="http://forum.java.sun.com/thread.jspa?threadID=721445">
+ Unless you are using Java 1.5,</a>
+ actions cannot be specified as a Welcome page.
+ So, in the case of a Welcome page,
+ how do we follow the best practice of navigating through actions
+ rather than pages?
+</p>
+
+<p>
+ One solution is to use a page to "bootstrap" one of our actions.
+ We can register the usual "index.html" as the Welcome page and have it
+ redirect to a "Welcome" action.
+</p>
+
+<hr/>
+<h5>MailReader's index.html</h5>
+<pre><code><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html><head>
+ <META HTTP-EQUIV="Refresh" CONTENT="0;<strong>URL=Welcome.do</strong>">
+ </head>
+ <body>
+ <p>Loading ...</p>
+</body></html></code></pre>
+<hr/>
+
+<p>
+ As an alternative,
+ we could also have used a JSP page that issued the redirect with a Struts tag,
+ but a plain HTML solution works as well.
+</p>
+
+<h4><a name="Welcome.do" id="Welcome.do">Welcome.do</a></h4>
+
+<p>
+ When the client requests "Welcome.do", the request is passed to the "struts2"
+ FilterDispatcher (that we registered in the web.xml file).
+ The FilterDispatcher retrieves the appropriate action mapping from the
+ configuration.
+ If we just wanted to forward to the Welcome page, we could use a simple
+ configuration element.
+</p>
+<hr/>
+<h5>A simple "forward thru" action element</h5>
+<pre><code><action name="<strong>Welcome</strong>">
+ <result><strong>/pages/Welcome.jsp</strong></result>
+</action></code></pre>
+<hr/>
+
+<p>
+ If a client asks for the Welcome action ("Welcome.do"), the "/page/Welcome.jsp"
+ page would be returned in response.
+ The client does not know, or need to know, that the physical resource is located at
+ "/pages/Welcome.jsp".
+ All the client knows is that it requested the resource "Welcome.do".
+</p>
+
+<p>
+ But if we peek at the configuration file for the MailReader,
+ we find a slightly more complicated XML element for the Welcome action.
+</p>
+
+<hr/>
+<h5>The Welcome action element</h5>
+<pre><code><action name="Welcome" <b>class="org.apache.struts.examples.mailreader2.Welcome"</b>>
+ <result>/pages/Welcome.jsp</result>
+ <strong><interceptor-ref name="guest"/></strong>
+ </action></code></pre>
+<hr/>
+
+<p>
+ Here, the <strong>Welcome</strong> Java class executes whenever
+ someone asks for the Welcome action.
+ As it completes, the Action class can select which "result" is displayed.
+ The default result name is "success".
+ Another available result, defined at a global scope, is "error".
+</p>
+
+<hr/>
+<h5>Key concept:</h5>
+<blockquote>
+ <p>
+ The Action class doesn't need to know what result type is needed
+ for "success" or "error".
+ The Action can just return the logical name for a result,
+ without knowing how the result is implemented.
+ </p>
+</blockquote>
+<hr/>
+
+<p>
+ The net effect is that all of the result details,
+ including the paths to server pages,
+ all can be declared <em>once</em> in the configuration.
+ Tightly coupled implementation details are not scattered all over
+ the application.
+</p>
+
+<hr/>
+<h5>Key concept:</h5>
+<blockquote>
+ <p>
+ The Struts configuration lets us separate concerns and "say it once".
+ The configuration helps us "normalize" an application,
+ in much the same way we normalize a database schema.
+ </p>
+</blockquote>
+<hr/>
+
+
+<p>
+ OK ... but why would a Welcome Action want to choose between "success" and
+ "error"?
+</p>
+
+<h4><a name="Welcome.java" id="Welcome.java">Welcome Action</a></h4>
+
+<p>
+ The MailReader application retains a list of users along with their email
+ accounts.
+ The application stores this information in a database.
+ If the application can't connect to the database, the application can't do
+ its job.
+ So before displaying the Welcome <strong>page</strong>, the Welcome
+ <strong>class</strong> checks to see if the database is available.
+</p>
+
+<p>
+ The MailReader is also an internationalized application.
+ So, the Welcome Action class checks to see if the message resources are
+ available too.
+ If both resources are available, the class passes back the "success" token.
+ Otherwise, the class passes back the "error" token,
+ so that the appropriate messages can be displayed.
+</p>
+
+<hr/>
+<h5>The Welcome Action class</h5>
+<pre><code>package mailreader2;
+public class Welcome extends MailreaderSupport {
+
+ public String execute() {
+
+ // Confirm message resources loaded
+ String message = getText(Constants.ERROR_DATABASE_MISSING);
+ if (Constants.ERROR_DATABASE_MISSING.equals(message)) {
+ <strong>addActionError(Constants.ERROR_MESSAGES_NOT_LOADED);</strong>
+ }
+
+ // Confirm database loaded
+ if (null==getDatabase()) {
+ <strong>addActionError(Constants.ERROR_DATABASE_NOT_LOADED);</strong>
+ }
+
+ if (hasErrors()) {
+ <strong>return ERROR;</strong>
+ }
+ else {
+ <strong>return SUCCESS;</strong>
+ }
+ }
+}</code></pre>
+<hr/>
+
+<p>
+ Several common result names are predefined,
+ including ERROR, SUCCESS, LOGIN, NONE, and INPUT,
+ so that these tokens can be used consistently across Struts 2 applications.
+</p>
+
+
+<h4><a name="global-results" id="global-results">Global Results</a></h4>
+
+<p>
+ As mentioned, "error" is defined in a global scope.
+ Other actions may have trouble connecting to the database later,
+ or other unexpected errors may occur.
+ The MailReader defines the "error" result as a Global Result,
+ so that any action can use it.
+</p>
+
+<hr/>
+<h5>MailReader's global-result element</h5>
+<pre><code> <global-results>
+ <result name=<strong>"error"</strong>><strong>/pages/Error.jsp</strong></result>
+ <result name="invalid.token">/pages/Error.jsp</result>
+ <result name="login" type="redirect-action">Login_input</result>
+</global-results></code></pre>
+<hr/>
+
+<p>
+ Of course, if an individual action mapping defines its own "error" result type,
+ the local result would be used instead.
+</p>
+
+<h4><a name="ApplicationListener.java" id="ApplicationListener.java">ApplicationListener.java</a>
+</h4>
+
+<p>
+ The database is exposed as an object stored in application scope.
+ The database object is based on an interface.
+ Different implementations of the database could be loaded without changing
+ the rest of the application.
+ But how is the database object loaded in the first place?
+</p>
+
+<p>
+ The database is created by a custom Listener that we configured in the "web.xml".
+</p>
+
+<hr/>
+<h5>org.apache.struts.examples.mailreader2.ApplicationListener</h5>
+<pre><code> <listener>
+ <listener-class>
+ <strong>org.apache.struts.examples.mailreader2.ApplicationListener</strong>
+ </listener-class>
+</listener></code></pre>
+<hr/>
+
+<p>
+ By default, our ApplicationListener loads a <strong>MemoryDatabase</strong>
+ implementation of the UserDatabase.
+ MemoryDatabase stores the database content as a XML document,
+ which is parsed and loaded as a set of nested hashtables.
+ The outer table is the list of user objects, each of which has its own
+ inner hashtable of subscriptions.
+ When you register, a user object is stored in this hashtable.
+ When you login, the user object is stored within the session context.
+</p>
+
+<p>
+ The database comes seeded with a sample user.
+ If you check the "database.xml" file under "/src/main/resources",
+ you'll see the sample user described in XML.
+</p>
+
+<hr/>
+<h5>The "seed" user element from the MailReader database.xml</h5>
+<pre><code><user username="<strong>user</strong>" fromAddress="John.User@somewhere.com"
+ fullName="<strong>John Q. User</strong>" password="<strong>pass</strong>">
+ <subscription host="<strong>mail.hotmail.com"</strong> autoConnect="false"
+ password="bar" type="pop3" username="user1234">
+ </subscription>
+ <subscription host="<strong>mail.yahoo.com</strong>" autoConnect="false" password="foo"
+ type="imap" username="jquser">
+ </subscription>
+</user></code></pre>
+<hr/>
+
+<p>
+ The "seed" user element creates a registration record for "John Q. User",
+ with the subscription detail for his hotmail and yahoo accounts.
+</p>
+
+<h4><a name="resources.properties" id="resources.properties">Message Resources</a>
+</h4>
+
+<p>
+ As mentioned, MailReader is an internationalized application.
+ In Struts 2, message resources are associated with the Action class being processed.
+ If we check the source, we find a language resource bundle named
+ <em>MailreaderSupport.</em>
+ MailreaderSupport is our base class for all the MailReader Actions.
+ Since all of our Actions extend MailreaderSupport,
+ all of our Actions can use the same resource bundle.
+</p>
+
+<hr/>
+<h5>Message Resource entries used by the Welcome page</h5>
+<pre><code><strong>index.heading=</strong>MailReader Application Options
+<strong>index.login=</strong>Log on to the MailReader Application
+<strong>index.registration=</strong>Register with the MailReader Application
+<strong>index.title=</strong>MailReader Demonstration Application
+<strong>index.tour=</strong>A Walking Tour of the MailReader Demonstration Application</code></pre>
+<hr/>
+
+<p>
+ If you change a message in the resource, and then rebuild and reload the
+ application, the change will appear throughout the application.
+ If you provide message resources for additional locales, you can
+ localize your application.
+ The MailReader provides resources for English, Russian, and Japanese.
+</p>
+
+<h4><a name="Welcome.jsp" id="Welcome.jsp">Welcome Page</a></h4>
+
+<p>
+ After confirming that the necessary resources exist, the Welcome action
+ forwards to the Welcome page.
+</p>
+<hr/>
+<h5>Welcome.jsp</h5>
+<pre><code><%@ page contentType="text/html; charset=UTF-8" %>
+<strong><%@ taglib prefix="s" uri="http://struts.apache.org/tags" %></strong>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <title><strong><s:text name="index.title"/></strong></title>
+ <link href="<strong><s:url value="/css/mailreader.css"/></strong>" rel="stylesheet"
+ type="text/css"/>
+ </head>
+
+ <body>
+ <h3><s:text name="index.heading"/></h3>
+
+ <ul>
+ <li><a href="<s:url action="Registration_input"/>"><s:text
+ name="index.registration"/></a></li>
+ <li><a href="<s:url action="Login_input"/>"><s:text
+ name="index.login"/></a></li>
+ </ul>
+
+ <h3>Language Options</h3>
+ <ul>
+ <li>
+ <s:url id="en" action="Welcome">
+ <s:param name="request_locale">en</s:param>
+ </s:url>
+ <s:a href="%{en}">English</s:a>
+ </li>
+ <li>
+ <s:url id="ja" action="Welcome">
+ <s:param name="request_locale">ja</s:param>
+ </s:url>
+ <s:a href="%{ja}">Japanese</s:a>
+ </li>
+ <li>
+ <s:url id="ru" action="Welcome">
+ <s:param name="request_locale">ru</s:param>
+ </s:url>
+ <s:a href="%{ru}">Russian</s:a>
+ </li>
+ </ul>
+
+ <hr />
+
+ <p><strong><s:i18n name="alternate"></strong>
+ <img src="<s:text name="struts.logo.path"/>"
+ alt="<s:text name="struts.logo.alt"/>"/>
+ <strong></s:i18n></strong></p>
+
+ <p><a href="<s:url action="Tour" />"><s:text name="index.tour"/></a></p>
+
+ </body>
+</html></code></pre>
+<hr/>
+
+<p>
+ At the top of the Welcome page, there are several directives that load the
+ Struts 2 tag libraries.
+ These are just the usual red tape that goes with any JSP file.
+ The rest of the page utilizes three Struts JSP tags:
+ "text", "url", and "i18n".
+</p>
+
+<p>
+ (We use the tag prefix "s:" in the Struts 2 MailReader application,
+ but you can use whatever prefix you like in your applications.)
+</p>
+
+<p>
+ The <strong>text</strong> tag inserts a message from an
+ application's default resource bundle.
+ If the framework's locale setting is changed for a user,
+ the text tag will render messages from the new locale's resource
+ bundle instead.
+</p>
+
+<p>
+ The <strong>url</strong> tag can render a reference to an
+ action or any other web resource,
+ applying "URL encoding" to the hyperlinks as needed.
+ Java's URL encoding feature lets your application maintain client state
+ without requiring cookies.
+</p>
+
+<hr/>
+<h5>Tip:</h5>
+<blockquote>
+ <p><font class="hint">
+ <strong>Cookies</strong> -
+ If you turn cookies off in your browser, and then reload your browser
+ and this page,
+ you will see the links with the Java session id information attached.
+ (If you are using Internet Explorer and try this,
+ be sure you reset cookies for the appropriate security zone,
+ and that you disallow "per-session" cookies.)
+ </font></p>
+</blockquote>
+<hr/>
+
+<p>
+ The <strong>i18n</strong> tag provides access to multiple resource bundles.
+ The MailReader application uses a second set of message resources for
+ non-text elements.
+ When these are needed, we use the "i18n" tag to specify a different bundle.
+</p>
+
+<p>
+ The <strong>alternate</strong> bundle is stored in the {{/src/main/resources}} folder,
+ so that it ends up under "classes", which is on the application's class path.
+</p>
+
+<p>
+ In the span of a single request for the Welcome page, the framework has done
+ quite a bit already:
+</p>
+
+<ul>
+ <li>
+ Confirmed that required resources were loaded during initialization.
+ </li>
+
+ <li>
+ Written all the page headings and labels from internationalized
+ message resources.
+ </li>
+
+ <li>
+ Automatically URL-encoded paths as needed.
+ </li>
+</ul>
+
+<p>
+ When rendered, the Welcome page lists two menu options:
+ one to register with the application and one to log on (if you have
+ already registered).
+ Let's follow the Login link first.
+</p>
+
+<h3><a name="Login" id="Login">Login</a></h3>
+
+<p>
+ If you choose the Login link, and all goes well, the Login action forwards
+ control to the Login page.
+</p>
+
+<h4><a name="Login.jsp" id="Login.jsp">Login Page</a></h4>
+
+<p>
+ The Login page displays a form that accepts a username and password.
+ You can use the default username and password to login
+ (<strong>user</strong> and <strong>pass</strong>), if
+ you like. Try omitting or misspelling the username and password in
+ various combinations to see how the application reacts.
+ Note that both the username and password are case sensitive.
+</p>
+
+<hr/>
+<h5>Login.jsp</h5>
+<pre><code><%@ page contentType="text/html; charset=UTF-8" %>
+ <%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title><s:text name="login.title"/></title>
+ <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+ </head>
+ <body onLoad="self.focus();document.Login.username.focus()">
+ <strong><s:actionerror/></strong>
+ <strong><s:form action="Login" validate="true"></strong>
+ <strong><s:textfield key="username"/></strong>
+ <strong><s:password key="password"/></strong>
+ <strong><s:submit key="button.save"/></strong>
+ <strong><s:reset key="button.reset"/></strong>
+ <s:submit <strong>action="Login_cancel" onclick="form.onsubmit=null"</strong>
+ key="button.cancel"/>
+ </s:form>
+ <jsp:include page="Footer.jsp"/>
+ </body>
+</html></code></pre>
+<hr/>
+
+<p>
+ We already saw some of the tags used by the Login page on the Welcome page.
+ Let's focus on the new tags.
+</p>
+
+<p>
+ The first new tag on the Login page is <strong>actionerrors</strong>.
+ Most of the possible validation errors are related to a single field.
+ If you don't enter a username,
+ the framework can place an error message near the tag prompting you to
+ enter a username.
+ But some messages are not related to a single field.
+ For example, the database might be down.
+ If the action returns an "Action Error", as opposed to a "Field Error",
+ the messages are rendered in place of the "actionerror" tag.
+ The text for the validation errors, whether they are Action Errors or
+ Field Errors, can be specified in the resource bundle,
+ making the messages easy to manage and localize.
+</p>
+
+<p>
+ The second new tag is <strong>form</strong>.
+ This tag renders a HTML form tag.
+ The "validate=true" setting enables client-side validation,
+ so that the form can be validated with JavaScript before being sent
+ back to the server.
+ The framework will still validate the form again, just to be sure, but the
+ client-side validation can save a few round-trips to the server.
+</p>
+
+<p>
+ Within the form tag,
+ we see four more new tags: "textfield", "password", "submit",
+ and "reset". We also see a second usage of "submit" that utilizes an
+ "action" attribute.
+</p>
+
+<p>
+ When we place a control on a form, we usually need to code a set of
+ HTML tags to do everything we want to do.
+ Most often, we do not just want a plain "input type=text" tag.
+ We want the input field to have a label too, and maybe even
+ a tooltip. And, of course, a place to print a message
+ should invalid data be entered.
+</p>
+
+<p>
+ The Struts Tags support templates and themes so that a set of HTML tags can be
+ rendered from a single Struts Tag. For example, the single tag
+</p>
+
+<pre><code>
+ <s:<strong>textfield</strong> key="username"/>
+</code></pre>
+
+<p>
+ generates a wad of HTML markup.
+</p>
+
+<hr/>
+<pre><code><tr>
+ <td class="tdLabel">
+ <label for="Login_username" class="label">Username:</label>
+ </td>
+ <td>
+ <input type="text" name="username" value="" id="Login_username"/>
+ </td>
+</tr></code></pre>
+<hr/>
+
+<p>
+ If for some reason you don't like the markup generated by a Struts Tag,
+ it's each to change.
+ Each tag is driven by a template that can be updated on a tag-by-tag basis.
+ For example,
+ here is the default template that generates the markup for the ActionErrors tag:
+</p>
+
+<hr/>
+<pre><code><#if (actionErrors?exists && actionErrors?size > 0)>
+ <ul>
+ <#list actionErrors as error>
+ <li><span class="errorMessage">${error}</span></li>
+ </#list>
+ </ul>
+</#if></code></pre>
+<hr/>
+
+<p>
+ If you wanted ActionErrors displayed in a table instead of a list,
+ you could edit a copy of this file, save it as a file named
+ "template/simple/actionerror.ftl",
+ and place this one file at the base of your application's classpath.
+</p>
+
+<hr/>
+<pre><code><#if (actionErrors?exists && actionErrors?size > 0)>
+ <strong><table></strong>
+ <#list actionErrors as error>
+ <strong><tr><td></strong><span class="errorMessage">${error}</span><strong></td></tr></strong>
+ </#list>
+ <strong></table></strong>
+</#if></code></pre>
+<hr/>
+
+<p>
+ Under the covers, the framework uses
+ <a href="http://freemarker.sourceforge.net/">Freemarker</a>
+ for its standard templating language.
+ FreeMarker is similar to
+ <a href="http://jakarta.apache.org/velocity/">Velocity</a>,
+ but it offers better error reporting and some additional features.
+ If you prefer, Velocity and JSP templates can also be used to create your own tags.
+</p>
+
+<p>
+ The <strong>password</strong> tag renders a "input type=password"
+ tag, along with the usual template/theme markup.
+ By default, the password tag will not retain input if the submit fails.
+ If the username is wrong,
+ the client will have to enter the password again too.
+ (If you did want to retain the password when validation fails,
+ you can set the tag's "showPassword" property to true.)
+</p>
+
+<p>
+ Unsurprisingly, the <strong>submit</strong> and <strong>reset</strong> tags
+ render buttons of the corresponding types.
+</p>
+
+<p>
+ The second submit button is more interesting.
+</p>
+
+<pre><code> <s:submit <strong>action="Login_cancel" onclick="form.onsubmit=null"</strong>
+ key="button.cancel"/>
+</code></pre>
+
+<p>
+ Here we are creating the Cancel button for the form.
+ The button's attribute <em>action="Login<strong>_</strong>cancel"</em>
+ tells the framework to submit to the Login's "cancel" method
+ instead of the usual "execute" method.
+ The <em>onclick="form.onsubmit=null"</em> script defeats client-side validation.
+ On the server side, "cancel" is on a special list of methods that bypass validation,
+ so the request will go directly to the Action's <strong>cancel</strong> method.
+ Another entry on the special-case list is the "input" method.
+</p>
+
+<hr/>
+<h5>Tip:</h5>
+<blockquote>
+ <p><font class="hint">
+ The Struts Tags have options and capabilities beyond what we have shown here.
+ For more see, the <a href="http://cwiki.apache.org/WW/tag-developers-guide.html">
+ Struts Tag documentation.</a>
+ </font></p>
+</blockquote>
+<hr/>
+
+<p>
+ OK, but how do the tags know that both of these fields are required?
+ How do they know what message to display when the fields are empty?
+</p>
+
+<p>
+ For the answers, we need to look at another flavor of configuration file:
+ the "validation" file.
+</p>
+
+<h4><a name="Login-validation.xml" id="Login-validation.xml">Login-validation.xml</a>
+</h4>
+
+<p>
+ While it is not hard to code data-entry validation into an Action class,
+ the framework provides an even easier way to validate input.
+</p>
+
+<p>
+ The validation framework is configured through another XML document, the <strong>
+ Login-validation.xml</strong>.
+</p>
+
+<hr/>
+<h5>Validation file for Login Action</h5>
+<pre><code><!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
+ "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
+<validators>
+ <field name="<strong>username</strong>">
+ <field-validator type="<strong>requiredstring</strong>">
+ <message key="<strong>error.username.required</strong>"/>
+ </field-validator>
+ </field>
+ <field name="<strong>password</strong>">
+ <field-validator type="<strong>requiredstring</strong>">
+ <message key="<strong>error.password.required</strong>"/>
+ </field-validator>
+ </field>
+</validators>
+</code></pre>
+<hr/>
+
+<p>
+ You may note that the DTD refers to "XWork".
+ <a href="http://www.opensymphony.com/xwork/">
+ Open Symphony XWork
+ </a> is a generic command-pattern framework that can be used outside of a
+ web environment. Essentially, Struts 2 is a web-based extension of the
+ XWork framework.
+</p>
+
+<p>
+ The field elements correspond to the ActionForm properties.
+ The <strong>username</strong> and <strong>password</strong> field elements
+ say that each field depends on the "requiredstring" validator.
+ If the username is blank or absent, validation will fail and an error
+ message is generated.
+ The messages would be based on the "error.username.required" or
+ "error.password.required" message templates from the resource bundle.
+</p>
+
+<!--
+<p>
+ The <strong>password</strong> field (or property) is also required.
+ In addition, it must also pass the "maxlength" and "minlength"
+ validations.
+ Here, the minimum length is three characters and the maximum length is
+ sixteen.
+ If the length of the password doesn't meet these criteria, a corresponding
+ error message is generated.
+ Of course, the messages are generated from the MessageResource bundles and
+ are easy to localize.
+</p>
+-->
+
+<h4><a name="Login.java" id="Login.java">Login Action</a></h4>
+
+<p>
+ If validation passes, the framework invokes the "execute" method of the Login Action.
+ The actual Login Action is brief, since most of the functionality derives
+ from the base class, <strong>MailreaderSupport</strong>.
+</p>
+
+<hr/>
+<h5>Login.java</h5>
+<pre><code>package mailreader2;
+import org.apache.struts.apps.mailreader.dao.User;
+public final class <strong>Login</strong> extends MailreaderSupport {
+public String <strong>execute()</strong> throws ExpiredPasswordException {
+ User user = <strong>findUser(getUsername(), getPassword());</strong>
+ if (user != null) {
+ <strong>setUser(user);</strong>
+ }
+ if (<strong>hasErrors()</strong>) {
+ return INPUT;
+ }
+ return SUCCESS;
+ }
+}</code></pre>
+<hr/>
+
+<p>
+ Login lays out what we do to authenticate a user.
+ We try to find the user using the credentials provided.
+ If the user is found, we cache a reference.
+ If the user is not found, we return "input" so the client can try again.
+ Otherwise, we return "success", so that the client can access the rest of the application.
+</p>
+
+<h4><a name="MailreaderSupport.java" id="MailreaderSupport.java">MailreaderSupport.java</a></h4>
+
+<p>
+ Let's look at the relevant properties and methods from MailreaderSupport
+ and another base class, <strong>ActionSupport</strong>, namely
+ "getUsername", "getPassword", "findUser", "setUser", and "hasErrors".
+</p>
+
+<p>
+ The framework lets you define
+ <a href="http://struts.apache.org/primer.html#javabeans">JavaBean properties</a>
+ directly on the Action.
+ Any JavaBean property can be used, including rich objects.
+ When a request comes in,
+ any public properties on the Action class are matched with the request parameters.
+ When the names match, the request parameter value is set to the JavaBean property.
+ The framework will make its best effort to convert the data,
+ and, if necessary, it will report any conversion errors.
+</p>
+
+<p>
+ The <strong>Username</strong> and <strong>Password</strong> properties are nothing fancy,
+ just standard JavaBean properties.
+</p>
+
+<hr/>
+<h5>MailreaderSupport.getUsername() and getPassword()</h5>
+<pre><code>private String username = null;
+public String <strong>getUsername()</strong> {
+ return this.username;
+}
+public void setUsername(String username) {
+ this.username = username;
+}
+
+private String password = null;
+public String <strong>getPassword()</strong> {
+ return this.password;
+}
+public void setPassword(String password) {
+ this.password = password;
+}</code></pre>
+<hr/>
+
+<p>
+ We use these properties to capture the client's credentials,
+ and pass them to the more interesting <strong>findUser</strong> method.
+</p>
+
+<hr/>
+<h5>MailreaderSupport.findUser</h5>
+<pre><code>public User <strong>findUser</strong>(String username, String password)
+ throws <strong>ExpiredPasswordException</strong> {
+ User user = <strong>getDatabase().findUser(username)</strong>;
+ if ((user != null) && !user.getPassword().equals(password)) {
+ user = null;
+ }
+ if (user == null) {
+ this.<strong>addFieldError</strong>("password", getText("error.password.mismatch"));
+ }
+ return user;
+}</code></pre>
+<hr/>
+
+<p>
+ The "findUser" method dips into the MailReader Data Access Object layer,
+ which is represented by the <strong>Database</strong> property.
+ The code for the DAO layer is maintained as a separate component.
+ The MailReader application imports the DAO JAR,
+ but it is not responsible for maintaining any of the DAO source.
+ Keeping the data access layer at "arms-length" is a very good habit.
+ It encourages a style of development where the data access layer
+ can be tested and developed independently of a specific end-user application.
+ In fact, there are several renditions of the MailReader application,
+ all which share the same MailReader DAO JAR!
+</p>
+
+<hr/>
+<h5>Best Practice:</h5>
+<blockquote>
+ <p>
+ <font class="hint">"Strongly separate data access and business logic from the rest of
+ the application."</font>
+ </p>
+</blockquote>
+<hr/>
+
+<p>
+ When "findUser" returns,
+ the Login Action looks to see if a valid (non-null) User object is returned.
+ A valid User is passed to the <strong>User property</strong>.
+ Although it is still a JavaBean property,
+ the User property is not implemented in quite the same way as Username and Password.
+</p>
+
+<hr/>
+<h5>MailreaderSupport.setUser</h5>
+<pre><code>public User getUser() {
+ return (User) <strong>getSession().get(Constants.USER_KEY)</strong>;
+}
+public void setUser(User user) {
+ getSession().put(Constants.USER_KEY, user);
+}</code></pre>
+<hr/>
+
+<p>
+ Instead of using a field to store the property value,
+ "setUser" passes it to a <strong>Session</strong> property.
+</p>
+
+<hr />
+<h5>MailreaderSupport.getSession() and setSession()</h5>
+<pre><code>private Map session;
+public Map <strong>getSession()</strong> {
+ return session;
+
+public void <strong>setSession(Map value)</strong> {
+ session = value;
+}</code></pre>
+<hr />
+
+<p>
+ To look at the MailreaderSupport class,
+ you would think the Session property is a plain-old Map.
+ In fact,
+ the Session property is an adapter that is backed by the servlet session object at runtime.
+ The MailreaderSupport class doesn't need to know that though.
+ It can treat Session like any other Map.
+ We can also test the MailreaderSupport class by passing it some other implementation of
+ Map, running the test,
+ and then looking to see what changes MailreaderSupport made to our "mock" Session object.
+</p>
+
+<p>
+ But, when MailreaderSupport is running inside a web application,
+ how does it acquire a reference to the servlet session?
+</p>
+
+<p>
+ Good question. If you were to look at just the MailreaderSupport class,
+ you would not see a single line of code that sets the session property.
+ But, yet, when we run the class, the session property is not null.
+ Hmmm.
+</p>
+
+<p>
+ The magic that provides the Session property a runtime value is called
+ "dependency injection".
+ The MailreaderSupport class implements a interface called <strong>SessionAware</strong>.
+ SessionAware is bundled with the framework,
+ and it defines a setter for the Session property.
+</p>
+
+<p>
+ <code>public void <strong>setSession</strong>(Map session);</code>
+</p>
+
+<p>
+ Also bundled with the framework is an object called the
+ <strong>ServletConfigInterceptor</strong>.
+ If the ServletConfigInterceptor sees that an Action implements the SessionAware interface,
+ it automatically set the session property.
+</p>
+
+<pre><code>if (action instanceof <code>SessionAware</code>) {
+ ((SessionAware) action).<code>setSession</code>(context.getSession());
+}</code></pre>
+
+<p>
+ The framework uses these "Interceptor" classes to create a <strong>front controller</strong>
+ for each action an application defines.
+ Each Interceptor can peek at the request before an Action class is invoked,
+ and then again after the Action class is invoked.
+ (If you have worked with Servlet
+ <a href="http://struts.apache.org/primer.html#filters">Filters</a>,
+ you will recognize this pattern.
+ But, unlike Filters, Interceptors are not tied to HTTP.
+ Interceptors can be tested and developed outside of a web application.)
+</p>
+
+<p>
+ You can use the same set of Interceptors for all your actions,
+ or define a special set of Interceptors for any given action,
+ or define different sets of Interceptors to use with different types of actions.
+ The framework comes with a default set of Interceptors,
+ that it will use when another set is not specified,
+ but you can designate your own default Interceptor set (or "stack")
+ in the Struts configuration.
+</p>
+
+<p>
+ Many Interceptors provide a utility or helper functions,
+ like setting the session property.
+ Others, like the <strong>ValidationInterceptor</strong>,
+ can change the workflow of an action.
+ Interceptors are key feature of the framework,
+ and we will see a few more on the tour.
+</p>
+
+<p>
+ If a valid User is not found, or the password doesn't match,
+ the "findUser" method invokes the <strong>addFieldError</strong> method to note the
+ problem.
+ When "findUser" returns, the Login Action checks for errors,
+ and then it returns either INPUT or SUCCESS.
+</p>
+
+<p>
+ The "addFieldError" method is provided by the ActionSupport class,
+ which is bundled with the framework.
+ The constants for INPUT and SUCCESS are also provided by ActionSupport.
+ While the ActionSupport class provides many useful utilities,
+ you are not required to use it as a base class.
+ Any Java class can be used as an Action, if you like.
+</p>
+
+<p>
+ It is a good practice to provide a base class with utilities
+ that can be shared by an application's Action classes.
+ The framework does this with ActionSupport,
+ and the MailReader application does the same with the MailreaderSupport class.
+</p>
+
+<hr/>
+<h5>Best Practice:</h5>
+<blockquote>
+ <p><font class="hint">"Use a base class to define common functionality."</font></p>
+</blockquote>
+<hr/>
+
+<p>
+ But, what happens if Login returns INPUT instead of SUCCESS.
+ How does the framework know what to do next?
+</p>
+
+<p>
+ To answer that question,
+ we need to turn back to the Struts configuration
+ and look at how Login is declared.
+</p>
+
+
+<h4><a name="Login.xml" id="Login.xml">Login Configuration</a></h4>
+
+<p>
+ The Login action element outlines how the Login workflow operates,
+ including what to do when the Action returns "input",
+ or the default result name "success".
+</p>
+
+<hr/>
+<h5>mailreader-support.xml Login</h5>
+<pre><code><action name="<strong>Login_*</strong>" method="{1}" class="mailreader2.Login">
+ <result name="<strong>input</strong>">/pages/Login.jsp</result>
+ <result name="<strong>cancel</strong>" type="redirect-action">Welcome</result>
+ <result type="redirect-action">MainMenu</result>
+ <result name="<strong>expired</strong>" type="chain">ChangePassword</result>
+ <<strong>exception-mapping</strong>
+ exception="org.apache.struts.apps.mailreader.dao.ExpiredPasswordException"
+ result="<strong>expired</strong>"/>
+ <interceptor-ref name="<strong>guest</strong>"/>
+</action></code></pre>
+<hr/>
+
+<p>
+ You might notice that the name of the Login action element is not "Login"
+ but "Login<strong>_*</strong>".
+ The asterisk is a special "wildcard" notation that tells the framework to match any series
+ of character at this point.
+ In the method attribute,
+ the "{1}" notation indicates that framework should substitute whatever characters match
+ the asterisk at runtime.
+ When we cite actions like "Login_cancel" or "Login_input",
+ the framework matches "cancel" or "input" with the wildcard and fills in the blanks.
+</p>
+
+<p>
+ The "trailing bang" notation was hardwired into WebWork 2.
+ To provide backward compatibility,
+ the notation is supported by Struts 2.0.
+ If you prefer to use wildcards to emulate the same notation,
+ as the Mailreader does,
+ you should disable the old notation in the Struts properties file.
+</p>
+
+<hr/>
+<h5>struts.properties</h5>
+<pre><code>struts.enable.DynamicMethodInvocation = false</code></pre>
+<hr/>
+
+<p>
+ Using wildcards with a exclamation point (or "bang") is not the only way we can use
+ wilcards to invoke methods.
+ If we wanted to use actions like "inputLogin",
+ we could move the asterisk and use an action name like "*Login".
+</p>
+
+<p>
+ Within the Login action element, the first result element is named "input".
+ If validation or authentification fail,
+ the Action class will return "input" and the framework will transfer control to the
+ "Login.jsp" page.
+</p>
+
+<p>
+ The second result element is named <strong>cancel</strong>.
+ If someone presses the cancel button on the Login page,
+ the Action class will return "cancel", this result will be selected,
+ and the framework will issue a redirect to the Welcome action.
+</p>
+
+<p>
+ The third result has no name,
+ so it will be called if the default <strong>success</strong> token is returned.
+ So, if the Login succeeds,
+ control will transfer to the MainMenu action.
+</p>
+
+<p>
+ The MailReader DAO exposes a "ExpiredPasswordException".
+ If the DAO throws this exception when the User logs in,
+ the framework will process the exception-mapping
+ and transfer control to the "ChangePassword" action.
+</p>
+
+<p>
+ Just in case any other Exceptions are thrown,
+ the MailReader application also defines a global handler.
+</p>
+
+<hr/>
+<h5>mailreader-default.xml exception-mapping</h5>
+<pre><code><global-exception-mappings>
+ <exception-mapping
+ result="error"
+ exception="java.lang.Exception"/>
+</global-exception-mappings></code></pre>
+<hr/>
+
+<p>
+ If an unexpected Exception is thrown,
+ the exception-mapping will transfer control to the action's "error" result,
+ or to a global "error" result.
+ The MailReader defines a global "error" result
+ which transfers control to an "Error.jsp" page
+ that can display the error message.
+</p>
+
+<hr/>
+<h5>Error.jsp</h5>
+<pre><code><%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title>Unexpected Error</title>
+ </head>
+ <body>
+ <h2>An unexpected error has occured</h2>
+ <p>
+ Please report this error to your system administrator
+ or appropriate technical support personnel.
+ Thank you for your cooperation.
+ </p>
+ <hr />
+ <h3>Error Message</h3>
+ <strong><s:actionerror /></strong>
+ <p>
+ <strong><s:property value="%{exception.message}"/></strong>
+ </p>
+ <hr />
+ <h3>Technical Details</h3>
+ <p>
+ <strong><s:property value="%{exceptionStack}"/></strong>
+ </p>
+ <jsp:include page="Footer.jsp"/>
+ </body>
+</html></code></pre>
+<hr/>
+
+<p>
+ The Error page uses <strong>property</strong> tags to expose
+ the Exception message and the Exception stack.
+</p>
+
+<p>
+ Finally, the Login action specifies an <strong>InterceptorStack</strong>
+ named <strong>defaultStack.</strong>
+ If you've worked with Struts 2 or WebWork 2 before, that might seem strange,
+ since "defaultStack" is the factory default.
+</p>
+
+<p>
+ In the MailReader application, most of the actions are only available
+ to authenticated users.
+ The exceptions are the Welcome, Login, and Register actions
+ which are available to everyone.
+ To authenticate clients,
+ the MailReader uses a custom Interceptor and a custom Interceptor stack.
+</p>
+
+<hr/>
+<h5>org.apache.struts.examples.mailreader2.AuthenticationInterceptor</h5>
+<pre><code>package mailreader2;
+import com.opensymphony.xwork2.interceptor.Interceptor;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.Action;
+import java.util.Map;
+import org.apache.struts.apps.mailreader.dao.User;
+
+public class <strong>AuthenticationInterceptor</strong> implements Interceptor {
+ public void destroy () {}
+ public void init() {}
+ public String <strong>intercept</strong>(ActionInvocation actionInvocation) throws Exception {
+ Map session = actionInvocation.getInvocationContext().getSession();
+ User user = (User) session.get(Constants.USER_KEY);
+ boolean isAuthenticated = (null!=user) && (null!=user.getDatabase());
+ if (<strong>isAuthenticated</strong>) {
+ return actionInvocation.invoke();
+ }
+ else {
+ return Action.LOGIN;
+ }
+ }
+}</code></pre>
+<hr/>
+
+<p>
+ The <strong>AuthenticationInterceptor</strong> looks to see if a User object
+ has been stored in the client's session state.
+ If so, it returns normally, and the next Interceptor in the set would be invoked.
+ If the User object is missing, the Interceptors returns "login".
+ The framework would match "login" to the global result,
+ and transfer control to the Login action.
+</p>
+
+<p>
+ The MailReader defines three custom Interceptor stacks: "user", "user-submit",
+ and "guest".
+</p>
+
+<hr/>
+<h5>mailreader-default.xml interceptors</h5>
+<pre><code><interceptors>
+ <interceptor name="<strong>authentication</strong>"
+ class="mailreader2.AuthenticationInterceptor"/>
+ <interceptor-stack name="<strong>user</strong>" >
+ <interceptor-ref name="authentication" />
+ <interceptor-ref name="defaultStack"/>
+ </interceptor-stack>
+ <interceptor-stack name="<strong>user-submit</strong>" >
+ <interceptor-ref name="tokenSession" />
+ <interceptor-ref name="user"/>
+ </interceptor-stack>
+ <interceptor-stack name="<strong>guest</strong>" >
+ <interceptor-ref name="defaultStack"/>
+ </interceptor-stack>
+</interceptors>
+<<strong>default-interceptor-ref</strong> name="user"/></code></pre>
+<hr/>
+
+<p>
+ The <strong>user</strong> stacks require that the client be authenticated.
+ In other words, that a User object is present in the session.
+ The actions using a <strong>guest</strong> stack can be accessed by any client.
+ The <strong>-submit</strong> versions of each can be used with actions
+ with forms, to guard against double submits.
+</p>
+
+<h5>Double Submits</h5>
+
+<p>
+ A common problem with designing web applications is that users are impatient
+ and response times can vary.
+ Sometimes, people will press a submit button a second time.
+ When this happens, the browser submits the request again,
+ so that we now have two requests for the same thing.
+ In the case of registering a user, if someone does press the submit button
+ again, and their timing is bad,
+ it could result in the system reporting that the username has already been
+ used.
+ (The first time the button was pressed.)
+ In practice, this would probably never happen, but for a longer running
+ process, like checking out a shopping cart,
+ it's easier for a double submit to occur.
+</p>
+
+<p>
+ To forestall double submits, and "back button" resubmits,
+ the framework can generate a token that is embedded in the form
+ and also kept in the session.
+ If the value of the tokens do not compare,
+ then we know that there has been a problem,
+ and that a form has been submitted twice or out of sequence.
+</p>
+
+<p>
+ The Token Session Interceptor will also attempt to provide intelligent
+ fail-over in the event of multiple requests using the same session.
+ That is, it will block subsequent requests until the first request is complete,
+ and then instead of returning the "invalid.token" code,
+ it will attempt to display the same response that the
+ original, valid action invocation would have displayed
+</p>
+
+<p>
+ Because the default interceptor stack will now authenticate the client,
+ we need to specify the standard "defaultStack" for the three
+ "guest actions", Welcome, Login, and Register.
+ Requiring authentification by default is the better practice, since it
+ means that we won't forget to enable it when creating new actions.
+ Meanwhile, those pesky users will ensure that we don't forget to disable
+ authentification for "guest" services.
+</p>
+
+<h3><a name="MainMenu" id="MainMenu">MainMenu</a></h3>
+
+<p>
+ On a successful login, the Main Menu page displays.
+ If you logged in using the demo account,
+ the page title should be "Main Menu Options for John Q. User".
+ Below this legend should be two links:
+</p>
+
+<ul>
+ <li>
+ Edit your user registration profile
+ </li>
+ <li>
+ Log off MailReader Demonstration Application
+ </li>
+</ul>
+
+<p>
+ Let's review the source for the "MainMenu" action mapping,
+ and the "MainMenu.jsp".
+</p>
+
+<hr/>
+<h5>Action mapping element for MainMenu</h5>
+<pre><code><action name="MainMenu" class="org.apache.struts.examples.mailreader2.MailreaderSupport">
+ <result>/pages/MainMenu.jsp</result>
+ </action></code></pre>
+
+<h5>MainMenu.jsp</h5>
+<pre><code><%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title><s:text name="mainMenu.title"/></title>
+ <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+ </head>
+
+ <body>
+ <h3><s:text name="mainMenu.heading"/> <strong><s:property
+ value="user.fullName"/></strong></h3>
+ <ul>
+ <li><a href="<s:url <strong>action="Registration_input"</strong> />">
+ <s:text name="mainMenu.registration"/>
+ </a>
+ </li>
+ <li><a href="<s:url <strong>action="Logout"</strong> />">
+ <s:text name="mainMenu.logout"/>
+ </a>
+ </ul>
+ </body>
+</html></code></pre>
+<hr/>
+
+<p>
+ The source for "MainMenu.jsp" also contains a new tag, <strong>
+ property</strong>, which we use to customize the page with the
+ "fullName" property of the authenticated user.
+</p>
+
+<p>
+ Displaying the user's full name is the reason the MainMenu action
+ references the MailreaderSupport class.
+ The MailreaderSupport class has a User property that the text tag
+ can access.
+ If we did not utilize MailreaderSupport,
+ the property tag would not be able to find the User object to print
+ the full name.
+</p>
+
+<p>
+ The customized MainMenu page offers two standard links.
+ One is to "Edit your user registration profile".
+ The other is to "Logout the MailReader Demonstration Application".
+</p>
+
+<h3><a name="Registration.jsp" id="Registration.jsp">Registration page</a>
+</h3>
+
+<p>
+ If you follow the "Edit your user registration profile" link from the Main
+ Menu page,
+ we will finally reach the heart of the MailReader application: the
+ Registration, or "Profile", page.
+ This page displays everything MailReader knows about you
+ (or at least your login),
+ while utilizing several interesting techniques.
+</p>
+
+<p>
+ To do double duty as the "Create" Registration page and the "Edit"
+ Registration page,
+ the "Registration.jsp" makes extensive use of the test tags,
+ to make it appears as though there are two distinct pages.
+</p>
+
+<hr />
+<h5>Registration.jsp - head element</h5>
+<pre><code><head>
+ <s:if test="<strong>task=='Create'</strong>">
+ <title><s:text name="registration.title.create"/></title>
+ </s:if>
+ <s:if test="<strong>task=='Edit'</strong>">
+ <title><s:text name="registration.title.edit"/></title>
+ </s:if>
+ <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+</head></code></pre>
+<hr />
+
+<p>
+ For example, if client is editing the form (task == 'Edit'),
+ the page inserts the username from the User object.
+ For a new Registration (task == 'Create'),
+ the page creates an empty data-entry field.
+</p>
+
+<hr/>
+<h5>Note:</h5>
+<blockquote>
+ <p><font class="hint">
+ <strong>Presention Logic</strong> -
+ The "test" tag is a convenient way to express presentation
+ logic within your pages.
+ Customized pages help to prevent user error,
+ and dynamic customization reduces the number of server pages your
+ application needs to maintain, among other benefits.
+ </font></p>
+</blockquote>
+<hr/>
+
+<p>
+ The page also uses logic tags to display a list of subscriptions
+ for the given user.
+ If the RegistrationForm has task set to "Edit",
+ the lower part of the page that lists the subscriptions is exposed.
+</p>
+
+<hr/>
+<h5></h5>
+<pre><code><s:if test=<strong>"task == 'Edit'"</strong>>
+ <div align="center">
+ <h3><s:text name="heading.subscriptions"/></h3>
+ </div>
+ <!-- ... -->
+ </s:if>
+<jsp:include page="Footer.jsp"/>
+</body></html></code></pre>
+<hr/>
+
+<p>
+ Otherwise, the page contains just the top portion --
+ a data-entry form for managing the user's registration.
+</p>
+
+<h4><a name="iterator" id="iterator">iterator</a></h4>
+
+<p>
+ Besides "if" there are several other control tags that you can use
+ to sort, filter, or iterate over data.
+ The Registration page includes a good example of using the <strong>iterator</strong>
+ tag to display the User's Subscriptions.
+</p>
+
+<p>
+ The subscriptions are stored in a hashtable object, which is in turn
+ stored in the user object.
+ So to display each subscription, we have to reach into the user object,
+ and loop through the members of the subscription collection.
+ Using the iterator tag, you can code it the way it sounds.
+ </p>
+
+<hr/>
+<h5>Using iterator to list the Subscriptions</h5>
+<pre><code><s:iterator value="<strong>user.subscriptions</strong>">
+ <tr>
+ <td align="left">
+ <s:property value="<strong>host</strong>"/>
+ </td>
+ <td align="left">
+ <s:property value="<strong>username</strong>"/>
+ </td>
+ <td align="center">
+ <s:property value="<strong>type</strong>"/>
+ </td>
+ <td align="center">
+ <s:property value="<strong>autoConnect</strong>"/>
+ </td>
+ <td align="center">
+ <a href="<s:url action="<strong>Subscription_delete</strong>"><s:param name="<strong>host</strong>" value="host"/></s:url>">
+ <s:text name="registration.deleteSubscription"/>
+ </a>
+ <a href="<s:url action="<strong>Subscription_edit</strong>"><s:param name="<strong>host</strong>" value="host"/></s:url>">
+ <s:text name="registration.editSubscription"/>
+ </a>
+ </td>
+ </tr>
+</s:iterator></code></pre>
+<hr/>
+
+<p>
+ When the iterator renders, it generates a list of Subscriptions for the current User.
+</p>
+
+<hr />
+
+ <div align="center">
+ <h3>Current Subscriptions</h3>
+ </div>
+
+ <table border="1" width="100%">
+ <tr>
+ <th align="center" width="30%">
+ Host Name
+ </th>
+ <th align="center" width="25%">
+ User Name
+ </th>
+
+ <th align="center" width="10%">
+ Server Type
+ </th>
+ <th align="center" width="10%">
+ Auto
+ </th>
+ <th align="center" width="15%">
+ Action
+ </th>
+ </tr>
+ <tr>
+ <td align="left">
+ mail.hotmail.com
+ </td>
+ <td align="left">
+ user1234
+ </td>
+ <td align="center">
+ pop3
+ </td>
+
+ <td align="center">
+ false
+ </td>
+ <td align="center">
+ <a href="/struts2-mailreader/Subscription_delete.do?host=mail.hotmail.com">
+ Delete
+ </a>
+
+ <a href="/struts2-mailreader/Subscription_edit.do?host=mail.hotmail.com">
+ Edit
+ </a>
+ </td>
+ </tr>
+ <tr>
+ <td align="left">
+ mail.yahoo.com
+ </td>
+ <td align="left">
+ jquser
+ </td>
+ <td align="center">
+ imap
+ </td>
+ <td align="center">
+ false
+ </td>
+ <td align="center">
+ <a href="/struts2-mailreader/Subscription_delete.do?host=mail.yahoo.com">
+ Delete
+ </a>
+
+ <a href="/struts2-mailreader/Subscription_edit.do?host=mail.yahoo.com">
+ Edit
+ </a>
+ </td>
+ </tr>
+ </table>
+ <a href="/struts2-mailreader/Subscription_input.do">Add</a>
+
+<hr />
+
+ <p>
+ Now look back at the code used to generate this block.
+ </p>
+ <p>
+ Notice anything nifty?
+ </p>
+ <p>
+ How about that the markup between the iterator tag is
+ actually <em>simpler</em> than the markup that we would use to render one row of the
+ table?
+ </p>
+ <p>
+ Instead of using a qualified reference like "value=user.subscription[0].host",
+ we use the simplest possible reference: "value=host".
+ We didn't have to define a local variable, and reference that local in the loop code.
+ The reference to each item in the list is automatically resolved, no fuss, no muss.
+ </p>
+ <p>
+ Nice trick!
+ </p>
+
+<p>
+ The secret to this magic is the <strong>value stack</strong>.
+ Next to Interceptors, the value stack is probably the coolest thing there is about the
+ framework.
+ To explain the value stack, let's step back and start from the beginning.
+</p>
+
+<p>
+ Merging dynamic data into static web pages is a primary reason
+ we create web applications.
+ The Java API has a mechanism that allows you to
+ place objects in a servlet scope (page, request, session, or
+ application), and then retrieve them using a JSP scriplet.
+ If the object is placed directly in one of the scopes,
+ a JSP tag or scriptlet can find that object by searching page scope and
+ then request scope, and session scope, and finally application scope.
+</p>
+
+<p>
+ The value stack works much the same way, only better.
+ When you push an object on the value stack,
+ the public properties of that object become first-class properties of the stack.
+ The object's properties become the stack's properties.
+ If another object on the stack has properties of the same name,
+ the last object pushed onto the stack wins. (Last-In, First-Out.)
+</p>
+
+<p>
+ When the iterator tag loops through a collection,
+ it pushes each item in the collection onto the stack.
+ The item's properties become the stack's property.
+ In the case of the Subscriptions,
+ if the Subscription has a public Host property,
+ then during that iteration,
+ the stack can access the same property.
+</p>
+
+<p>
+ Of course, at the end of each iteration, the tag "pops" the item off the stack.
+ If we were to try and access the Host property later in the page,
+ it won't be there.
+</p>
+
+<p>
+ When an Action is invoked, the Action class is pushed onto the value stack.
+ Since the Action is on the value stack,
+ our tags can access any property of the Action
+ as if it were an implicit property of the page.
+ The tags don't access the Action directly.
+ If a textfield tag is told to render the "Username" property,
+ the tag asks the value stack for the value of "Username",
+ and the value stack returns the first property it finds by that name,
+ on any object on the stack.
+</p>
+
+<p>
+ The Validators also use the stack.
+ When validation fails on a field,
+ the value for the field is pushed onto the value stack.
+ As a result, if the client enters text into an Integer field,
+ the framework can still redisplay whatever was entered.
+ An invalid input value is not stored in the field (even if it could be).
+ The invalid input is pushed onto the stack for the scope of the request.
+</p>
+
+<p>
+ The Subscription list uses another new tag: the <strong>param</strong> tag.
+ As tags go, "param" takes very few parameters of its own: just "name" and "value",
+ and neither is required.
+ Although simple, "param" is one of the most powerful tags the framework provides.
+ Not so much because of what it does,
+ but because of what "param" allows the other tags to do.
+</p>
+
+<p>
+ Essentially, the "param" tag provides parameters to other tags.
+ A tag like "text" might be retrieving a message template with several replaceable
+ parameters.
+ No matter how many parameters are in the template, and no matter what they are named,
+ you can use the "param" tag to pass in whatever you need.
+</p>
+
+<pre><code>pager.legend = Displaying {current} of {count} items matching {criteria}.
+...
+<s:text name="pager.legend">
+ <s:<strong>param</strong> name="current" value="42" />
+ <s:<strong>param</strong> name="count" value="314" />
+ <s:<strong>param</strong> name="criteria" value="Life, the Universe, and Everything" />
+</s:text></code></pre>
+
+<p>
+ In the case of an "url" tag,
+ we can use "param" to create the query string.
+ A statement like this:
+</p>
+
+<pre><code>
+ <s:url action="Subscription_edit"><s:param name="<strong>host" value="host</strong>"/></s:url>">
+</code></pre>
+
+<p>
+ can render a hyperlink like this:
+</p>
+
+<pre><code>
+ <a href="/struts2-mailreader/Subscription_edit.do?<strong>host=mail.yahoo.com</strong>">Edit</a>
+</code></pre>
+
+<!--
+<p>
+ At the foot of the Register page is a link for adding a subscription.
+ Let's wind up the tour by following the Add link and then logging off.
+ Like the link for creating a Registration, Add points to an "Edit" action,
+ namely "EditSubscription".
+</p>
+-->
+
+<p>
+ If a hyperlink needs more parameters,
+ you can use "param" to add as many parameters as needed.
+</p>
+
+<h3>
+ <a name="Subscription" id="Subscription">Subscription</a>
+</h3>
+
+<p>
+ If we follow one of the "Edit" subscription links on the Registration page,
+ we come to the Subscriptions page,
+ which displays the details of our description in a data-entry form.
+ Let's have a look at the Subscription configuration
+ and follow the bouncing ball from page to action to page.
+</p>
+
+<hr />
+<h5>mailreader-support.xml Subscription element</h5>
+<pre><code><action name="Subscription_*" method="{1}" class="org.apache.struts.examples.mailreader2.Subscription">
+ <result name="input">/pages/Subscription.jsp</result>
+ <result type="redirect-action">Registration_input</result>
+</action></code></pre>
+<hr />
+
+<p>
+ The Edit link specified the Subscription action,
+ but also includes the qualifier <strong>_edit</strong>.
+ The wildcard notation tells the framework to use any characters given after "Subscription_"
+ as the name of a method to invoke on the Action class,
+ instead of the default execute method.
+ The "alternate" execute methods are called <strong>alias</strong> methods.
+</p>
+
+<hr />
+<h5>Subscription edit alias</h5>
+<pre><code>public String <strong>edit()</strong> {
+ <strong>setTask(Constants.EDIT);</strong>>
+ return find();
+}
+
+public String find() {
+ org.apache.struts.apps.mailreader.dao.Subscription
+ sub = findSubscription();
+ if (sub == null) {
+ return ERROR;
+ }
+ <strong>setSubscription(sub);</strong>
+ return INPUT;
+}</code></pre>
+<hr />
+
+<p>
+ The "edit" alias has two responsibilities.
+ First, it must set the Task property to "Edit".
+ The Subscription page will render itself differently
+ depending on the value of the Task property.
+ Second, "edit" must locate the relevant Subscription
+ and set it to the Subscription property.
+ If all goes well, "edit" returns the INPUT token,
+ so that the "input" result will be invoked.
+</p>
+
+<p>
+ In the normal course, the Subscription should always be found,
+ since we selected the entry from a system-generated list.
+ If the Subscription is not found,
+ it would be because the database disappeared
+ or the request is being spoofed.
+ If the Subscription is not found,
+ edit returns the token for the global "error" result,
+ because this condition is unexpected.
+</p>
+
+<p>
+ The business logic for the "edit" alias is a simple wrapper
+ around the MailReader DAO classes.
+</p>
+
+<hr />
+<h5>MailreaderSupport findSubscription()</h5>
+<pre><code>public Subscription <strong>findSubscription()</strong> {
+ return findSubscription(getHost());
+}
+
+public Subscription findSubscription(String host) {
+ Subscription subscription;
+ subscription = <strong>getUser().findSubscription(host);</strong>
+ return subscription;
+}</code></pre>
+<hr />
+
+<p>
+ This code is very simple
+ and doesn't seem to provide much in the way of error handling.
+ But, that's OK.
+ Since the page is suppose to be entered from a link that we created,
+ we do expect everything to go right here.
+ But, if it doesn't, the global exception handler we defined in the
+ MailReader configuration will trap the exception for us.
+</p>
+
+<p>
+ Likewise, the AuthentificationInterceptor will ensure that only clients
+ with a valid User object can try to edit a Subscription.
+ If the session expired, or someone bookmarked the page,
+ the client will be redirected to the Login page automatically.
+</p>
+
+<p>
+ As a final layer of defense, we also configured a validator for Subscription,
+ to ensure that we are passed a Host parameter.
+</p>
+
+<hr />
+<h5>Subscription-validation.xml</h5>
+<pre><code><!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
+<validators>
+ <field name="<strong>host</strong>">
+ <field-validator type="<strong>requiredstring</strong>">
+ <message key="error.host.required"/>
+ </field-validator>
+ </field>
+</validators></code></pre>
+<hr />
+
+<p>
+ By keeping routine safety precautions out of the Action class,
+ the all-important Action becomes smaller and easier to maintain.
+</p>
+
+<p>
+ After setting the relevent Subscription object to the Subscription property,
+ the framework transfers control to the (you guessed it) Subscription page.
+</p>
+
+<hr />
+<h5>Subscription.jsp</h5>
+<pre><code><%@ page contentType="text/html; charset=UTF-8" %>
+<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <s:if test="task=='Create'">
+ <title><s:text name="subscription.title.create"/></title>
+ </s:if>
+ <s:if test="task=='Edit'">
+ <title><s:text name="subscription.title.edit"/></title>
+ </s:if>
+ <s:if test="task=='Delete'">
+ <title><s:text name="subscription.title.delete"/></title>
+ </s:if>
+ <link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
+ type="text/css"/>
+ </head>
+ <body onLoad="self.focus();document.Subscription.username.focus()">
+
+ <s:actionerror/>
+ <s:form <strong>action="Subscription_save"</strong> validate="true">
+ <strong><s:token /></strong>
+ <strong><s:hidden name="task"/></strong>
+ <strong><s:label key="username" name="user.username"/></strong>
+
+ <s:if test="task == 'Create'">
+ <s:textfield key="mailHostname" name="host"/>
+ </s:if>
+ <s:else>
+ <s:label key="mailHostname" name="host"/>
+ <s:hidden name="host"/>
+ </s:else>
+
+ <s:if test="task == 'Delete'">
+ <s:label key="subscription.username"/>
+ <s:label key="subscription.password"/>
+ <s:label key="subscription.type"/>
+ <s:label key="subscription.autoConnect"/>
+ <s:submit key="button.confirm"/>
+ </s:if>
+ <s:else>
+ <s:textfield key="subscription.username"/>
+ <s:textfield key="subscription.password"/>
+ <strong><s:select key="subscription.type" list="types"/></strong>
+ <strong><s:checkbox key="subscription.autoConnect"/></strong>
+ <s:submit key="button.save"/>
+ <s:reset key="button.reset"/>
+ </s:else>
+
+ <s:submit action="Registration_input"
+ key="button.cancel"
+ onclick="form.onsubmit=null"/>
+ </s:form>
+
+ <jsp:include page="Footer.jsp"/>
+ </body>
+</html></code></pre>
+<hr />
+
+<p>
+ As before, we'll discuss the tags and attributes that are new to this page:
+ "token", "hidden", "label", "select", and "checkbox".
+</p>
+
+<p>
+ The <strong>token</strong> tag works with the Token Session Interceptor to foil double
+ submits.
+ The tag generates a key that is embedded in the form and cached in the session.
+ Without this tag, the Interceptor can't work it's magic.
+</p>
+
+<p>
+ The <strong>hidden</strong> tag embeds the Task property into the form.
+ When the form is submitted,
+ the Subscription_save action will use the Task property to decide
+ whether to insert or update the form.
+</p>
+
+<p>
+ The <strong>label</strong> renders a "read only" version of a property,
+ suitable for placement in the form.
+ In Edit or Delete mode, we want the Host property to be immutable,
+ since it is used as a key. (As unwise as that might sound.)
+ In Delete mode, all of the properties are immutable,
+ since we are simply confirming the delete operation.
+</p>
+
+<p>
+ Saving the best for last, the Subscription form utilizes two more interesting
+ tags, "select" and "checkbox".
+</p>
+
+<p>
+ Unsurprisingly, the <strong>select</strong> tag renders a select control,
+ but the tag does so without requiring a lot of markup or redtape.
+</p>
+
+<pre><code><s:select key="subscription.type" <strong>list="types"</strong> />
+</code></pre>
+
+<p>
+ The interesting attribute of the "select" tag is "list",
+ which, in our case, specifies a value of "types".
+ If we take another look at the Subscription action,
+ we can see that it implements an interface named Preparable
+ and populates a Types property in a method named "prepare".
+</p>
+
+<hr />
+<h5>Subscription-validation.xml</h5>
+<pre><code>public class <strong>Subscription</strong> extends MailreaderSupport
+ <strong>implements Preparable</strong> {
+
+ private Map types = null;
+ public Map <strong>getTypes()</strong> {
+ return types;
+ }
+
+ public void <strong>prepare()</strong> {
+ Map m = new LinkedHashMap();
+ m.put("imap", "IMAP Protocol");
+ m.put("pop3", "POP3 Protocol");
+ types = m;
+ setHost(getSubscriptionHost());
+ }
+
+ // ... </code></pre>
+<hr />
+
+<p>
+ The default Interceptor stack includes the <strong>PrepareInterceptor</strong>,
+ which observes the Preparable interface.
+</p>
+
+<hr />
+<h5>PrepareInterceptor</h5>
+<pre><code>public class <strong>PrepareInterceptor</strong> extends AroundInterceptor {
+
+ protected void after(ActionInvocation dispatcher, String result) throws Exception {
+ }
+
+ protected void before(ActionInvocation invocation) throws Exception {
+ Object action = invocation.getAction();
+ <strong>if (action instanceof Preparable) {
+ ((Preparable) action).prepare();</strong>
+ }
+ }
+}</code></pre>
+
+<p>
+ The PrepareInterceptor ensures that the "prepare" method will always be called
+ before "execute" or an alias method is invoked.
+ We use "prepare" to setup the list of items for the select list to display.
+ We also transfer the Host property from our Subscription object
+ to a local property, where it is easier to manage.
+</p>
+
+<h4>
+ <a name="Subscription.java" id="Subscription.java">Subscription.java</a>
+</h4>
+
+<p>
+ Like many applications, the MailReader uses mainly String properties.
+ One exception is the AutoConnect property of the Subscription object.
+ On the HTML form, the AutoConnect property is represented by a checkbox.
+</p>
+
+<p>
+ When writing web applications, the checkbox can be a tricky control.
+ The Subscription object has a boolean AutoConnect property,
+ and the checkbox simply has to represent its state.
+ The problem is, if you clear a checkbox, the browser client will not submit <em>anything</em>.
+ Nada. Zip.
+ It is as if the checkbox control never existed.
+ The HTTP protocol has no way to affirm "false".
+ If the control is missing, we need to figure out it's been unclicked.
+</p>
+
+<p>
+ In Struts 1,
+ we use the <code>reset</code> method to work around checkbox issues.
+ In Struts 2, checkbox state is handled automatically.
+ The framework can detect when a checkbox tag has not been sent back,
+ and when that happens,
+ a default "false" value is used for the checkbox value.
+ No worries, mate.
+</p>
+
+<p>
+ If we press the SAVE button,
+ the form will be submitted to the Subscription_save action.
+ Since the save method needs some additional validation,
+ we can add a validation file.
+</p>
+
+<hr />
+<h5>Subscription-Subscription_save-validation.xml</h5>
+<pre><code><!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
+ "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
+<validators>
+ <field name="<strong>host</strong>">
+ <field-validator type="<strong>requiredstring</strong>">
+ <message key="error.host.required"/>
+ </field-validator>
+ </field>
+</validators></code></pre>
+<hr />
+
+<p>
+ The validators follow the same type of inheritance path as the classes.
+ SubscriptionSave extends Subscription,
+ so when Subscription_save is validated,
+ the Host property specified by "Subscription-validation.xml" will also be required.
+</p>
+
+<p>
+ If validation succeeds, the <code>save</code> method of Subscription will fire.
+</p>
+
+<hr />
+<h5>Subscription</h5>
+
+<pre><code>public String <strong>save</strong>() throws Exception {
+
+ if (Constants.DELETE.equals(getTask())) {
+ <strong>removeSubscription</strong>();
+ }
+
+ if (Constants.CREATE.equals(getTask())) {
+ <strong>copySubscription(</strong>getHost());
+ }
+
+ saveUser();
+ return SUCCESS;
+}</code></pre>
+<hr />
+
+<p>
+ The <strong>save</strong> method uses the Task property to handle
+ the special cases of deleting and creating,
+ and then updates the state of the User object.
+</p>
+
+<p>
+ The <strong>removeSubscription</strong> method calls the DAO facade,
+ and then updates the application state.
+</p>
+
+<hr />
+<h5>removeSubscription</h5>
+<pre><code>public void <strong>removeSubscription</strong>() throws Exception {
+ getUser().removeSubscription(getSubscription());
+ getSession().remove(Constants.SUBSCRIPTION_KEY);
+}</code></pre>
+<hr />
+
+<p>
+ The <strong>copySubscription</strong> method is a bit more interesting.
+ The MailReader DAO layer API includes some immutable fields
+ that can't be set once the object is created.
+ Because key fields are immutable,
+ we can't just create a Subscription, let the framework populate all the fields,
+ and then save it when we are done -- because some fields can't be populated,
+ except at construction.
+</p>
+
+<p>
+ One workaround would be to declare properties on the Action
+ for all the properties we need to pass to the Subscription or User objects.
+ When we are ready to create the object,
+ we could pass the new object values from the Action properties.
+</p>
+
+<p>
+ Another workaround is to declare only the immutable properties on the Action,
+ and then use what we can from the domain object.
+</p>
+
+<p>
+ This implementation of the MailReader utilizes the second alternative.
+ We define User and Subscription objects on our base Action,
+ and add other properties only as needed.
+</p>
+
+<p>
+ To add a new Subscription or User,
+ we create a blank object to capture whatever fields we can.
+ When this "input" object returns, we create a new object,
+ setting the immutable fields to appropriate values,
+ and copy over the rest of the properties.
+</p>
+
+<hr />
+<h5>copySubscription</h5>
+<pre><code>public void <strong>copySubscription</strong>(String host) {
+ Subscription input = getSubscription();
+ Subscription sub = createSubscription(host);
+ if (null != sub) {
+ <strong>BeanUtils.setValues</strong>(sub, input, null);
+ setSubscription(sub);
+ setHost(sub.getHost());
+ }
+}</code></pre>
+<hr />
+
+<p>
+ Of course, this is not a preferred solution,
+ but merely a way to work around an issue in the MailReader DAO API
+ that would not be easy for us change.
+</p>
+
+<h4>Subscription Submit</h4>
+
+<p>
+ When we pressed the SAVE button, there was one step that we overlooked.
+ The Mailreader application uses a "double submit" guard to keep people
+ from clicking the SAVE button multiple times and submitting the form again.
+</p>
+
+<p>
+ To add the double-submit guard, we can change the actions default processing
+ stack to <code>user-submit</code>.
+ But, we don't want to just copy and paste the other action settings from
+ the main Subscription action.
+ What we can do is put the subscription actions in their own package,
+ so that they can share result types.
+</p>
+
+<hr />
+<h5>mailreader-support.xml</h5>
+<pre><code><!-- ... -->
+</package>
+
+<package name="subscription" namespace="/" extends="mailreader-support">
+
+ <global-results>
+ <result name="input">/Subscription.jsp</result>
+ <result type="redirect-action">Registration_input</result>
+ </global-results>
+
+ <action name="Subscription_save" method="save" class="org.apache.struts.examples.mailreader2.Subscription">
+ <interceptor-ref name="user-submit" />
+ </action>
+
+ <action name="Subscription_*" method="{1}" class="org.apache.struts.examples.mailreader2.Subscription" />
+
+</package>
+
+<package name="wildcard" namespace="/" extends="mailreader-support">
+
+ <action name="*" class="org.apache.struts.examples.mailreader2.MailreaderSupport">
+ <result>/{1}.jsp</result>
+ </action>
+
+</package>
+}</code></pre>
+<hr />
+
+<p>
+ Aftering a successful save,
+ the Subscription Action will return "success",
+ and the framework will redirect us back to Registration input.
+</p>
+
+<h3>Summary</h3>
+<p>
+ At this point, we've booted the application, logged on,
+ reviewed a Registration record, and edited a Subscription.
+ Of course, there's more, but from here on, it is mostly more of the same.
+ The full source code for MailReader is
+ <a href="http://svn.apache.org/viewvc/struts/struts2/trunk/apps/mailreader/">
+ available online</a>
+ and in the distribution.
+</p>
+
+<p>
+ Enjoy!
+</p>
+</blockquote>
+</body>
+</html>
diff --git a/pom.xml b/pom.xml
index c8f00d3..73995dc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -89,6 +89,7 @@
<module>interceptors</module>
<module>json</module>
<module>json-customize</module>
+ <module>mailreader2</module>
<module>message-resource</module>
<module>message-store</module>
<module>portlet</module>