Chapter 12: Testing Struts Applications

Application-Managed Security

If container-managed security is not sufficient for your application, then you need to consider creating your own security structure. Generally, you need to address the same issues in application-managed security that are addressed in container-managed security:

  • How and when do you authenticate the users?

  • What determines the authorization levels?

  • Is there a guest user and, if so, what privileges does that user have?

  • Does the application need to support HTTPS for transmission of sensitive data?

The following sections look at several ways of handling these issues. As you read through them, you will apply your knowledge to the Mini HR application. Most of this discussion applies to Java-based Web applications in general-not just Struts-based applications.

Creating a Security Service

Using application-managed security requires different skills and knowledge than are required to use a container-managed approach. For application-managed security, you need to rely on traditional "best practices" development. For use with Mini HR, you will create a SecurityService interface API that will act as a façade for whatever underlying implementations exist. The definition of the interface to meet the needs of the Mini HR application is shown here:

package;     public interface SecurityService {   public User authenticate(String username, String password)     throws AuthenticationException; }

Next is a sample implementation that uses a HashMap to store the user data in memory. Of course, a real implementation would retrieve this data from a persistent store such as a file system, relational database, or LDAP server.

package;     import java.util.HashMap; import java.util.Map;     public class SecurityServiceImpl implements SecurityService {   private Map users;   private static final String ADMIN_ROLE = "administrator";       public SecurityServiceImpl() {     users = new HashMap();     users.put("bsiggelkow",       new User( "bsiggelkow","Bill", "Siggelkow",  "thatsme",         new String[] {ADMIN_ROLE}));     users.put("jholmes",       new User( "jholmes","James", "Holmes",  "maindude",         new String[] {ADMIN_ROLE}));     users.put("gburdell",       new User( "gburdell","George", "Burdell",  "gotech",         new String[] {ADMIN_ROLE}));   }       public User authenticate(String username, String password)     throws AuthenticationException   {     User user = (User) users.get(username);     if (user == null)       throw new AuthenticationException("Unknown user");     boolean passwordIsValid = user.passwordMatch(password);     if (!passwordIsValid)       throw new AuthenticationException("Invalid password");     return user;   } }

This interface provides the basic security services. The authenticate( ) method verifies the user's password and returns an object that represents the user. For the authorization needs of the application, there are different alternatives. While it may be tempting to engineer a complete role-based infrastructure, a more pragmatic approach is to provide a mechanism to determine if a user is an administrator. Following is the object that will hold the user data:

package;     import;     public class User implements Serializable {   private String firstName;   private String lastName;   private String username;   private String password;   private String[] roles;       public User(String name, String fName, String lName,     String pwd, String[] assignedRoles) {     username = name;     firstName = fName;     lastName  = lName;     password  = pwd     roles = assignedRoles;   }       public String getUsername() {     return username;   }       public String getFirstName() {     return firstName;   }       public String getLastName() {     return lastName;   }       public boolean passwordMatch(String pwd) {     return password.equals(pwd);   }       public boolean hasRole(String role) {     if (roles.length > 0) {       for (int i=0; i<roles.length; i++) {         if (role.equals(roles[i])) return true;       }     }     return false;   }       public boolean isAdministrator() {     return hasRole("administrator");   } }

Notice that some basic information that can be used for personalization, such as the user's first and last name, has been included. In addition, the isAdministrator( ) method indicates whether or not the user is an administrator. Also, a hasRole( ) method has been implemented that will indicate whether or not a user has been assigned a given role. This method will be useful with customized Struts role processing. To tie this custom security service into the application, you also need to create a LoginAction that will process the user login, as shown in the following code. A LogoutAction that allows a user to log out of the application is also created by the following code. A logout action typically needs to just invalidate the user's session. While you cannot force a user to log out, it is important to provide this feature, particularly if your application is accessible from a public terminal.

package com.jamesholmes.minihr;     import javax.servlet.http.*; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.*; import*;     public final class LoginAction extends Action {   public ActionForward execute(ActionMapping mapping,     ActionForm form,     HttpServletRequest request,     HttpServletResponse response)     throws Exception   {     HttpSession session = request.getSession();     String username =      (String) PropertyUtils.getSimpleProperty(form, "username");     String password =      (String) PropertyUtils.getSimpleProperty(form,"password");     SecurityService service = new SecurityServiceImpl();     User user = service.authenticate(username, password);     session.setAttribute("User", user);     // Forward control to this Action's success forward     return mapping.findForward("success");   } }

First, the action gets the username and password from the login form. Then, it calls the authenticate method of the security service. The returned user is then stored in the session. The exception handling is performed using Struts' declarative exception handling. Back at the index.jsp page, you need to change it from the container-managed implementation as shown in the following example. In this case, you want to check the administrator bean property of the user and display the link only if the value is true. Note also that you must explicitly check whether the User object is in the session. This handles the case where the user has not yet been authenticated.

<%@ taglib uri="" prefix="html" %> <%@ taglib uri="" prefix="logic" %>     <html> <head>   <title>ABC, Inc. Human Resources Portal</title> </head> <body> <font size="+1">ABC, Inc. Human Resources Portal</font><br>     <logic:notPresent name="user" scope="session"> <hr width="100%" noshade="true"> <html:form action="/login"> Username: <html:text property="username"/><br/> Password: <html:password property="password"/><br/> <html:submit value="Login"/> </html:form> <html:errors/> </logic:notPresent>     <hr width="100%" noshade="true"> <ul> <logic:present name="user" scope="session"> <logic:equal name="user" property="administrator" value="true"> <li><html:link forward="add">Add an Employee</html:link></li> </logic:equal> </logic:present> <li><html:link forward="search">Search for Employees</html:link></li> </ul> </body> </html> 

The important modification to this index page is the use of the <logic:present> tag to hide or show the login form. In addition, the <logic:equal> tag is used to hide or show the Add an Employee link. You are not finished, however. First, remove (or comment out) the container-managed security sections from the web.xml file. Then, remove the roles attribute from the action mapping.

You are still not done yet. Notice that there is now nothing to prevent someone who is not an administrator from adding a new employee. Anyone can browse directly to the Add an Employee page and submit the form. In addition, although securing the add.jsp page is not critical, it is imperative that you secure the AddAction ( What you need to do is implement the checks that were in place with container-managed security. There are several alternatives to consider.

Page/Action-Level Security Checks

You want to disallow access to the JSP page and, what is more important, to the AddAction ( The JSP page can be checked by adding in logic to the JSP, similar to the index.jsp page, that looks for the User object in the session and checks whether the user is an administrator. If the user is not an administrator, a <logic:redirect> tag can be used to send the user back to the index page. To protect the action, the most obvious solution is to add these same checks into the AddAction class. If the checks fail, the user can be redirected to a page where an error could be displayed, or an exception can be thrown, or an appropriate HTTP status (e.g., 400) can be set in the response. While this solution works for one JSP and one action, it will not scale when many pages and actions are involved.

One mechanism for making this solution scale for the action is to place the authorization check in a base Action class. If, however, you still want to use role-based access for authorization, a better solution is to extend the Struts' request processing engine.

Extending Struts' Request Processing Engine

Struts' request processing engine can be customized by inserting new commands into the request processing chain of ActionServlet which was discussed in Chapter 3. Specifically, security customizations can be brought into play by replacing the default AuthorizeAction chain command with a custom command. AuthorizeAction simply extends AbstractAuthorizeAction, providing an implementation for its abstract isAuthorized( ) method. The isAuthorized( ) method determines how roles, specified for an action mapping via the roles attribute, are handled. Its purpose is to ensure that a user who is accessing an action with assigned roles has at least one of those roles. The method returns true to continue processing normally or false to stop processing and return an appropriate response. The default implementation uses the HttpServletRequest.isUserInRole( ) method to determine if a user has a particular role. This can be used with container-managed security. For application-managed security, you need to override this method in a custom command that extends AbstractAuthorizeAction. For your implementation, you will get the User object from the session and then call the User.hasRole( ) method. The custom chain command with the overridden method is shown here:

package;     public class AuthorizeAction extends AbstractAuthorizeAction {   protected boolean isAuthorized(ActionContext context, String[] roles,     ActionConfig mapping)       throws Exception {     // Is this action protected by role requirements?     if ((roles == null) || (roles.length < 1)) {       return (true);     }         // Identify the HTTP request object     ServletActionContext servletActionContext =       (ServletActionContext) context;     HttpServletRequest request = servletActionContext.getRequest();         // Check the current user against the list of required roles     HttpSession session = request.getSession();     User user = (User) session.getAttribute("user");     if (user == null) {       return false;     }     for (int i = 0; i < roles.length; i++) {       if (user.hasRole(roles[i])) {         return (true);       }     }         // Default to unauthorized     return (false);   }       protected String getErrorMessage(ActionContext context,     ActionConfig actionConfig) {     ServletActionContext servletActionContext =       (ServletActionContext) context;         // Retrieve internal message resources     ActionServlet servlet = servletActionContext.getActionServlet();     MessageResources resources = servlet.getInternal();         return resources.getMessage("notAuthorized", actionConfig.getPath());   } }

In versions of Struts previous to 1.3, the RequestProcessor must be extended to customize request processing. Following is a custom request processor with the processRoles( ) method overridden to provide custom processing:

package;     import; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.struts.action.*; import;     public class CustomRequestProcessor extends RequestProcessor {   protected boolean processRoles(HttpServletRequest request,     HttpServletResponse response, ActionMapping mapping)     throws IOException, ServletException   {     // Is this action protected by role requirements?     String roles[] = mapping.getRoleNames();     if ((roles == null) || (roles.length < 1)) {       return (true);     }     // Check the current user against the list of required roles     HttpSession session = request.getSession();     User user = (User) session.getAttribute("user");     if (user == null) {       return false;     }     for (int i = 0; i < roles.length; i++) {       if (user.hasRole(roles[i])) {         return (true);       }     }     response.sendError(HttpServletResponse.SC_BAD_REQUEST,                        getInternal().getMessage("notAuthorized",                        mapping.getPath()));     return (false);   } }

Now, you can add the roles attribute back to your action mapping and the action will be protected. Of course, you also need to plug the custom chain command into the chain configuration file (or for previous versions of Struts, the customized request processor into the Struts configuration file). That still leaves the somewhat nagging problem of unauthorized users gaining access to the JSP pages. One solution is to place all the JSP pages under WEB-INF and route all JSP page requests through the ActionServlet by using ForwardActions. This is an appealing solution, because you can then use the roles attribute on these actions. This does mean a fairly drastic change to the organization of a Web application and may require substantial modification of your JSP pages. However, as you will see in the following section, servlet filters can be used to implement security policies that can be applied to related Web resources.

In addition to creating a custom AuthorizeAction command, you can create other commands that get called earlier in the chain request processing. That is a good way to put in security checks that cannot be implemented using roles alone. In versions of Struts previous to 1.3, the processPreprocess( ) method of the RequestProcessor gave this same flexibility.

Using Servlet Filters for Security

Servlet filters were introduced as part of the Servlet 2.3 specification. They provide a powerful mechanism for creating customized request and response processing that can be applied across many Web resources. You can create a filter and then map it to a collection of URLs by using the URL mapping discussed earlier in the chapter. Filters can alter a request before it arrives at its destination and, likewise, can modify the response after it leaves a destination. Filters can be applied to static HTML pages, JSP pages, Struts actions-essentially, any resource that you can specify with a URL.

You can use a filter to implement role-based access controls. The filter in effect performs the same checks that were implemented in the custom request processing code described in the previous section of this chapter. The filter determines if a user is allowed access to a given Web resource. It checks if the user has been authenticated and if the user has one of the required roles. If either of these checks fails, the filter stores an appropriate error message in the request and forwards the request to a URL. Initialization parameters are used to specify the authorization as well as the page to forward to if an error occurs. As you can see, the initialization parameters enable the creation of filter classes that can easily be reused. Here is the complete implementation for the authorization filter that Mini HR will be using:

package;     import; import javax.servlet.*; import javax.servlet.http.*; import org.apache.struts.Globals; import org.apache.struts.action.*;     public class AuthorizationFilter implements Filter {   private String[] roleNames;   private String onErrorUrl;       public void init(FilterConfig filterConfig)       throws ServletException {     String roles = filterConfig.getInitParameter("roles");     if (roles == null || "".equals(roles)) {       roleNames = new String[0];     }     else {       roles.trim();       roleNames = roles.split("\\s*,\\s*");     }     onErrorUrl = filterConfig.getInitParameter("onError");     if (onErrorUrl == null || "".equals(onErrorUrl)) {       onErrorUrl = "/index.jsp";     }   }       public void doFilter(ServletRequest request,                        ServletResponse response,                        FilterChain chain)                  throws IOException, ServletException {     HttpServletRequest req = (HttpServletRequest) request;     HttpServletResponse res = (HttpServletResponse) response;         HttpSession session = req.getSession();     User user = (User) session.getAttribute("user");     ActionErrors errors = new ActionErrors();     if (user == null) {       errors.add(ActionErrors.GLOBAL_ERROR,         new ActionMessage("error.authentication.required"));     }     else {       boolean hasRole = false;       for (int i=0; i<roleNames.length; i++) {         if (user.hasRole(roleNames[i])) {           hasRole = true;           break;         }       }       if (!hasRole) {         errors.add(ActionErrors.GLOBAL_ERROR,           new ActionMessage("error.authorization.required"));       }     }     if (errors.isEmpty()) {       chain.doFilter(request, response);     }     else {       req.setAttribute(Globals.ERROR_KEY, errors);       req.getRequestDispatcher(onErrorUrl).forward(req, res);     }   }       public void destroy() {   } }

First, notice that the AuthorizationFilter class implements Filter. Thus, it must implement the init( ), doFilter( ), and destroy( ) methods. The comma-separated list of roles and the URL of the error page to forward to are retrieved from the initialization parameters in the init( ) method. The doFilter( ) method first checks if there is a User object in the session. If not, an appropriate ActionMessage is created and no further checks are performed. Otherwise, it iterates through the list of roles to determine if the user has any of them. If not, an ActionMessage is created. If any errors were created, then a RequestDispatcher is created to forward to the given URL; otherwise, the doFilter( ) method calls chain.doFilter( ) to continue normal processing. This implementation also demonstrates the ability to integrate filters with Struts. You could have provided a more generic implementation, by calling the sendError( ) method of the HttpServletResponse class with an appropriate HTTP status code (e.g., 401).

Filters are configured and deployed like servlets. In the web.xml file, you can specify the filter name and class, and initialization parameters. Then, associate the filter with a URL pattern in a filter mapping. The following is the snippet from the web.xml file that shows the necessary changes for this filter:

<filter>   <filter-name>adminAccessFilter</filter-name>   <filter-class>   </filter-class>   <init-param>     <param-name>roles</param-name>     <param-value>administrator</param-value>   </init-param>   <init-param>     <param-name>onError</param-name>     <param-value>/index.jsp</param-value>   </init-param> </filter>     <filter-mapping>   <filter-name>adminAccessFilter</filter-name>   <url-pattern>/admin/*</url-pattern> </filter-mapping>

As you can see, the AuthorizationFilter is fairly generic. There is, in fact, an open-source security filter known as SecurityFilter ( that is worth considering. This filter permits implementation of a custom security policy, yet it still allows programmatic access to role and user information (i.e., the request.isUserInRole( ) and request.getUserPrincipal( ) methods) that is generally only available when using container-managed authentication. It performs this magic by using wrapper classes around the HttpServletRequest. SecurityFilter is configured using a separate configuration file that is very similar to the standard security-constraint element of the web.xml file. It provides the benefits of application-managed security while still enabling standard role processing without modification. There is no need to extend the Struts request processing engine for role handling, and the <logic:equal> tag with the role attribute will also work correctly. SecurityFilter is implemented similarly to AuthorizationFilter. You will want to understand how the filter works and its limitations before migrating your application.

Like servlets and Struts actions, filters are extremely powerful. You have complete access to the Java API that can be applied as needed using URL patterns. However, to truly integrate filters with Struts, you may need to delve into Struts implementation details as was shown in the AuthorizationFilter.

Using Cookies

A cookie consists of name-value data that can be sent to a client's browser and then read back again at a later time. Persistent cookies are stored by the client's browser. Cookies can be read only by the same server or domain that originated them. Also, a cookie can have an expiration period. Cookies are supported by most major browsers. However, cookies are often considered a privacy risk and can be disabled by the client. A good approach is to design your Web application to use cookies to improve the user experience, but not to require or force users to allow cookies.

For application-managed security, you can use cookies to allow users automatic logins. Specifically, you can create a persistent cookie that contains the user's username and password. Then, when a user accesses the application, you can check for those cookie values. If present, the values can be used to log the user in without requiring them to fill out a login form. Using a servlet filter, or some JavaScript, you could log in a user automatically. Alternatively, you may want to just prepopulate the login form with the values from the cookie.

To illustrate the use of cookies, Mini HR will be changed to use them as follows:

  1. Once a user logs in, Mini HR creates persistent cookies containing the username and password.

  2. Mini HR uses the cookie support of the Struts tags to set the initial values for the login form.

For the login action, this means adding the following lines after the authentication check has been performed:

Cookie usernameCookie = new Cookie("MiniHRUsername", username); usernameCookie.setMaxAge(60 * 60 * 24 * 30); // 30 day expiration response.addCookie(usernameCookie); Cookie passwordCookie = new Cookie("MiniHRPassword", password); passwordCookie.setMaxAge(60 * 60 * 24 * 30); // 30 day expiration response.addCookie(passwordCookie);

This code creates cookies for holding the username and password. Each cookie has an expiration of 30 days. Each cookie is then added to the response.

Next, use the Struts Bean Tag Library tags to retrieve the cookie values and write the values to the login form:

<logic:notPresent name="user" scope="session">   <bean:cookie  name="MiniHRUsername" value=""/>   <bean:cookie  name="MiniHRPassword" value=""/>   <hr width="100%" noshade="true">   <html:form action="/login">     Username: <html:text property="username"                value="<%=uname.getValue()%>"/><br/>     Password: <html:password property="password"                value="<%=pword.getValue()%>"/><br/>     <html:submit value="Login"/>   </html:form>   <html:errors/> </logic:notPresent>

The cookie tags retrieve the cookie values from the request and store them in page scope variables. These variables are then used as the initial values for the login form fields. However, this example is too simplistic for production use. Generally, using cookies without input from the user is considered overly presumptuous. A good Web application lets the user specify whether they want their user data stored as a cookie. It is also reasonable to let the user specify the length of time before the cookie expires. This type of information is easily gathered and stored by an application. Typically, this information is collected at registration time and stored as part of the user's profile.

In addition, the data sent in the cookies should be secured or encrypted. A simple encryption scheme, such as MD5 or a variant of the Secure Hash Algorithm (SHA), can be used to encrypt the cookie value when it is created. Since the server creates the cookie and is the only party to legitimately use the data, it can encrypt and decrypt the data using the algorithm of its own choosing. Alternatively, cookies can be configured to be transmitted only over HTTPS-thereby providing encryption/decryption at the transport level.

Integrating Struts with SSL

Web applications often need to allow certain operations to be performed under secure processing-that is, using HTTPS. Users have come to expect sensitive data such as their usernames, passwords, and credit card numbers to be transmitted over a secure channel. As was noted earlier, the use of HTTPS for specific URLs can be specified using a user data constraint within a security constraint in the web.xml file. This declarative mechanism can be used to restrict URLs to SSL (by specifying a transport guarantee of INTEGRAL or CONFIDENTIAL). However, this approach does not address all the issues when using SSL. As a container-managed service, the implementation and behavior with SSL can vary by container. If the service is not used carefully and with a full understanding of its nuances, it is easy to code an application that will only run in a specific container-even when using services that are defined via an industry-accepted specification.

Therefore, HTTPS typically is used only when passing sensitive data, and otherwise HTTP is used. This requires redirecting from nonsecure pages to secure pages and then back again. Performing this redirection requires changing the protocol scheme on a URL from http to https on each redirection. The biggest problem with needing to do this protocol switching is that absolute URLs must be hard-coded into JSP pages and Action classes. This quickly leads to deployment and maintenance problems that arise when server names are different between development, integration, test, and production servers. Some techniques for overcoming this problem are described shortly.

More pragmatically, programming an application to use HTTPS has other, more mundane but nevertheless equally frustrating issues. A common one is that the https protocol of the URL must often be hard-coded into a page. In fact, generally if you create HTML links that reference HTTPS, you must specify a fully qualified absolute URL. This makes it difficult to develop an application that is easy to migrate between deployment servers. Also, because switching the protocol requires an HTTP redirect, request attributes for the current request cannot be propagated to the secure URL. Thankfully, there is an open-source solution for handling these types of problems.

SSLEXT to the Rescue

The SSL Extension to Struts (SSLEXT) is an open-source plug-in for Struts. This software was created and is maintained by Steve Ditlinger (and others) and is hosted at SourceForge, It is the recommended approach for integrating Struts with SSL processing for Struts 1.2 and earlier. Unfortunately, at the time of this writing SSLEXT does not support Struts 1.3 because of its move to Jakarta Commons Chain for request processing. It is possible that SSLEXT will support later versions of Struts in the future, but there were not any committed timelines for that at the time of this writing.

SSLEXT's features include

  • The ability to declaratively specify in the Struts configuration file whether or not an action mapping should be secure. This feature allows your application to switch protocols between actions and JSP pages.

  • Extensions of the Struts JSP tags that can generate URLs that use the https protocol.

SSLEXT consists of a plug-in class for initialization, a custom extension to the Struts RequestProcessor, and a custom extension of the Struts ActionMapping. In addition, custom JSP tags, which extend the Struts tags, are provided for protocol-specific URL generation. SSLEXT also includes an additional JSP tag that lets you specify whether an entire JSP page is secure. SSLEXT depends on the Java Secure Socket Extension (JSSE), which is included with JDK 1.4 and later. Finally, you need to enable SSL for your application server. For Tomcat, this can be found in the Tomcat SSL How-To documentation.

SSLEXT works by intercepting the request in its SecureRequestProcessor. If the request is directed toward an action that is marked as secure, the SecureRequestProcessor generates a redirect. The redirect changes the protocol to https and the port to a secure port (e.g., 443 or 8443). This sounds simple enough; however, a request in a Struts application usually contains request attributes. These attributes are lost on a redirect. SSLEXT solves this problem by temporarily storing the request attributes in the session.

SSLEXT does not include a lot of documentation, but it comes with a sample application that demonstrates its use and features. To try SSLEXT, you can modify Mini HR to use it by changing the login behavior so that the LoginAction occurs over HTTPS. Once logged in, SSLEXT should switch the protocol back to HTTP. To set up SSLEXT for the Mini HR application, follow these steps:

  1. Copy the sslext.jar file into the MiniHR\WEB-INF\lib folder.

  2. Copy the sslext.tld file into the MiniHR\WEB-INF folder.

  3. Add a taglib declaration in the web.xml for the sslext tag library as follows:

    <taglib>   <taglib-uri>/WEB-INF/sslext.tld</taglib-uri>   <taglib-location>/WEB-INF/sslext.tld</taglib-location> </taglib>

Now, make the following changes to the struts-config.xml file:

  1. Add the type attribute to the action-mappings element to specify the custom secure action mapping class as follows:

    <action-mappings type="org.apache.struts.config.SecureActionConfig">

  2. Add the controller element configured to use the SecureRequestProcessor. If you are already using a custom request processor, change it to extend the SecureRequestProcessor.

    <controller   processor/>

  3. Add the plug-in declaration to load the SSLEXT code:

    <plug-in className="org.apache.struts.action.SecurePlugIn">   <set-property property="httpPort" value="8080"/>   <set-property property="httpsPort" value="8443"/>   <set-property property="enable" value="true"/>   <set-property property="addSession" value="true"/> </plug-in>
  4. Set the secure property to true for the login action mapping by adding the following element:

    <set-property property="secure" value="true"/>
  5. Finally, you need to configure the index.jsp page to always run on http, not https. Otherwise, after you log in, the protocol will remain on https. Add the following taglib directive and custom tag to the index.jsp page (after the existing taglib directives):

    <%@ taglib uri="/WEB-INF/tlds/sslext.tld" prefix="sslext"%> <sslext:pageScheme secure="false"/>

This tag is only needed for those JSP pages that are not accessed through your actions.

Now all you need to do is rebuild and redeploy the application. When you click the login link, the protocol will switch to https and the port will switch to 8443. After you log in, you should be redirected back to the index.jsp page and the protocol and port should switch back to http and 8080. You should experiment with using the <sslext:link> tag to create links to secure actions. You will find that using SSLEXT is much easier than using the user-data-constraint subelement of the web.xml file. It gives you fine-grained control where you need it through the tags. At the same time, it leverages the Struts configuration file to enable simple declarative configuration for secure request processing.

Struts. The Complete Reference
Struts: The Complete Reference, 2nd Edition
ISBN: 0072263865
EAN: 2147483647
Year: 2004
Pages: 165
Authors: James Holmes

Similar book on Amazon
Struts 2 in Action
Struts 2 in Action
Murach's Java Servlets and JSP, 2nd Edition
Murach's Java Servlets and JSP, 2nd Edition
Jakarta Struts For Dummies
Jakarta Struts For Dummies
Head First Servlets and JSP: Passing the Sun Certified Web Component Developer Exam
Head First Servlets and JSP: Passing the Sun Certified Web Component Developer Exam © 2008-2017.
If you may any questions please contact us: