Thursday, April 16, 2009

User authentication / registration with JSF / RichFaces, Spring, and Hibernate

In this article, I show you how to implement a Web 2.0-style user registration solution using JSF/RichFaces, Spring, and Hibernate. This article evolved from a real Web 2.0 site I'm building; I hope to document more lessons learned as I have time. You can learn more about me on my LinkedIn profile. As for what's in it for you, I've provided the source code and a working solution using:

In future articles, I also plan to incorporate Hibernate Search and implementations of collective intelligence techniques such as tag clouds, clustering, collaborative filtering, and recommendations.

While writing this article, I assumed the reader would have a basic understanding of the technologies listed above and focused more on how to integrate them to solve the requirements discussed in the next section.

Basic Requirements

Here are the high-level requirements this solution satisfies. Notice that most these requirements apply to many Web 2.0 sites seen on the Web today.

1. Public-facing Web site with read-only access to Guests
The site should be useful without logging in and the content should be accessible to search engines. Since casual surfers will be visiting the site, it must load fast and be easy to use on all major browsers (IE 6 / 7, FireFox 3.x, and Safari 3.x).

Site

2. Open registration process with email verification
Guests should be able sign-up for a new account using a simple form that collects minimal information from the user; CAPTCHAs should be used to prevent bots from creating accounts. Each user must agree to the site's terms of use before registering. After registering, the user will also need to click on an activation link sent to their email, which ensures they have access to the email account provided on the form.

Registration Form

3. Remember Me
Registered users can request the site to remember their credentials for automatic login for future visits.

Sign-In Form

4. Ability to recover forgotten passwords
Since we have a valid email for every user, users can request a new temporary password to be emailed to them. Upon logging in with the temporary password, the user should be prompted to change their password to a permanent value.

Recover Password Form Password Reset Confirmation Dialog Change Password Dialog

5. User Preferences
Registered users can share personal information, upload a photo, and setup site preferences, such as whether they are interested in receiving emails about special promotions from the site.

User Preferences

6. Strong Server-side Validation
Since we're exposing the site to the Wild Wild Web, server-side validation is essential; client-side validation using JavaScript is a nice to have, but is not sufficient. It's trivial for a hacker to by-pass client-side validation so you want to make sure your server code is protected as well. Where appropriate, use AJAX to give immediate feedback to the user when they've entered invalid data.

AJAX Validation

Layered Architecture

To satisfy these requirements, I've implemented a layered architecture depicted in the diagram below:

Layered Architecture Diagram

For simple authentication, Spring Security may seem like overkill. However, as the application evolves, I'm also going to need authorization. In addition, Spring Security supports OpenID, which is also of interest for this site.

Web Application Organization

Here is the link to the source code and WAR file. A simple Ant build script is provided so you can build the source. Before digging too deeply into the details of the configuration, take moment to familiarize yourself with the organization of the Web application:

Web Application Directory Structure

Persistence Layer
Domain Objects

There are three primary domain objects: User, Preferences, and Role, which should be self-explanatory given the requirements outlined above. The domain objects are POJOs with the exception of also using Hibernate annotations in a few key places. For example, in the following code snippet, we annotate the User class as a Hibernate Entity and map it to the ex_user table in the database:

@Entity
@Table(name="ex_user")
public class User implements Serializable ...

Notice that we are using surrogate keys for our domain objects that are auto-generated by Hibernate using the best approach for the specific database-type:

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getUserId() {
    return userId;
}

We also define the userName property to be the natural ID for the User entity. While rare, you can allow the userName to change, such as if a user gets married and wants to change her name.

@NaturalId(mutable=true)
public String getUserName() {
    return userName;
}

I'm also using the Hibernate Validator annotations to restrict the values for some of the members, such as the userName:

@NotEmpty
@Length(min=4,max=16)
@Pattern(regex="^[a-zA-Z\\d_]{4,12}$", message="Invalid screen name.")
private String userName;

Unfortunately, the default localized message for the @Pattern annotation isn't very user-friendly so I've had to resort to a hard-coded English message until I figure out a cleaner solution.

Domain Object Associations

There is a one-to-one relationship between the Preferences and User objects. The Preferences object is loaded with the User object (fetch=FetchType.EAGER) because it contains information that is needed to render the UI for the user.

@OneToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@PrimaryKeyJoinColumn
public Preferences getPreferences() {
    return preferences;
}

There is a bidirectional many-to-many relationship between the User and Role objects using a join table ex_user_role.

@ManyToMany(targetEntity=example.user.Role.class, cascade={CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name="ex_user_role",joinColumns=@JoinColumn(name="userId"), inverseJoinColumns=@JoinColumn(name="roleId"))
protected Set<Role> getUserRoles() {
    if (userRoles == null) userRoles = new HashSet<Role>();
    return userRoles;
}

On the Role object, we have:

@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy="userRoles", targetEntity=User.class)
public Set<User> getRoleUsers() {
    if (roleUsers == null) roleUsers = new HashSet<User>();
    return roleUsers;
}

This solution comes directly from the Hibernate documentation, section 7.5.3.

Data Access Object (DAO)

The persistence layer adopts the Data Access Object (DAO) design pattern to hide the details of the underlying persistence mechanism from the business logic of the application. For this solution, there are two DAO interfaces defined: UserDAO and RoleDAO.

The HibernateUserDao implements the UserDAO interface and extends Spring's HibernateDaoSupport base class. HibernateDaoSupport uses the Template method pattern to greatly simplify the process of using Hibernate correctly. Additionally, all checked Hibernate exceptions are translated into unchecked exceptions that extend from org.springframework.dao.DataAccessException. This allows developers to handle most persistence exceptions, which are non-recoverable, only in the appropriate layers, without having annoying boilerplate catch-and-throw blocks and exception declarations in the DAOs. For example, an attempt to insert a new record that violates a primary key constraint can only be resolved by changing the values the end-user is providing, such as a User ID during registration. With the Spring DataAccessException strategy, the DAO will throw a DataIntegrityViolationException and allow the presentation layer to catch it and prompt the end-user to change their input accordingly. Moreover, the Spring DataAccessException strategy insulates our business-logic layer from Hibernate specific classes. This is helpful if the underlying persistence mechanism needs to change for your application.

Hibernate

Each DAO implementation bean is injected with an instance of AnnotationSessionFactoryBean. This Spring FactoryBean provides a shared Hibernate SessionFactory and supports JDK 1.5+ annotation metadata for mappings, which alleviates the need to have hbm.xml files for our persistent objects.

<bean id="hbSessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
  <property name="dataSource" ref="jdbcDs">
  <property name="annotatedClasses">
    <list>
      <value>example.user.User</value>
      <value>example.user.Role</value>
      <value>example.user.Preferences</value>
    </list>
  </property>
  <property name="hibernateProperties">
    <value>hibernate.dialect=example.user.dao.hibernate.HSQLDialect_HHH_1598</value>
  </property>
  <property name="schemaUpdate" value="true">
</bean>

The hbSessionFactory bean is configured to auto-generate the database schema and is injected with our Spring DataSource. The custom hibernate.dialect property is provided to fix an issue with HSQLDB (see: http://forum.hibernate.org/viewtopic.php?p=2324183).

Under the covers, we use an Apache Commons DBCP connection pool that is exposed as a javax.sql.DataSource in the JNDI tree in Tomcat. A javax.sql.DataSource is defined by the JDBC specification to hide connection pooling and transaction management issues from application code. The web-application-config.xml file defines a reference to the javax.sql.DataSource using:

<bean class="org.springframework.jndi.JndiObjectFactoryBean" id="jdbcDs">
  <property name="jndiName" value="java:comp/env/jdbc/exampleDs">
</bean>

The database specific properties for the pool are configured in the META-INF/context.xml file:

<Resource name="jdbc/exampleDs"
          auth="Container"
          type="javax.sql.DataSource"
          username="sa"
          password=""
          driverClassName="org.hsqldb.jdbcDriver"
          url="jdbc:hsqldb:file:example"
          initialSize="0"
          maxActive="10"
          maxIdle="10"
          minIdle="0"
          maxWait="30000"
          poolPreparedStatements="true"
          />

Be sure to put the hsqldb.jar file into the tomcat/lib directory if you use HSQL.

Service Layer

One could allow the presentation layer beans to interact directly with the DAOs. However, this approach leads to your business logic being infected with presentation logic, which makes it difficult to support other interfaces, such as Web services. The Service Layer sits between your presentation layer and persistence layer. The Service Layer is where you implement the business-logic of your application.

For this solution, we have only one Spring bean in the Service Layer: UserService. The UserService provides a high-level interface to the operations provided by the UserDAO and RoleDAO. UserService is an application of the Session Facade design pattern in that it aggregates fine-grained data access operations into a single high-level transaction-aware service. Moreover, the UserService keeps business-logic out of the DAO implementations, which are only concerned with efficient object storage and retrieval.

Transactions

As mentioned above, the UserService is transaction-aware. This is accomplished using a Spring TransactionProxyFactoryBean which works with the HibernateTransactionManager. However, since we're using Spring 2.5.x and Java 5, we can use a more compact, annotation driven approach for making our UserServiceImpl transactional. First, you need to include the following element in your Spring configuration:

<tx:annotation-driven/>

Second, you need to mark your service's concrete implementation class with the @Transactional annotation:

@Transactional
public class UserServiceImpl implements UserService ...

For more information about declarative transactions with Spring, see: Declarative transaction management.

Mail Service

As described in the requirements, we'll need to send emails from time to time. For this, we'll be using Spring's JavaMailSender. The environment specific settings for your SMTP server are pulled from the example-env.properties file.

Presentation Layer

I chose JSF with RichFaces for this project based on the following criteria:

  • Modern, professional looking skin out-of-the-box (I don't want to fool with CSS).
  • Extensive set of easy-to-use, yet flexible UI components, such as trees, data grids, tabbed panes, etc.
  • Easy to create reusable custom components.
  • Large community of users and developers building real apps.
  • Performance: loads and runs fast on all modern browsers.
  • Minimizes hand-coded JavaScript.
RichFaces ModalPanel

I decided to use the RichFaces ModalPanel component <rich:modalPanel> for my dialogs because it helps the user keep their current orientation with the site. Instead of navigating to a different page, I simply open a dialog over the current view. Consequently, I didn't have any need for Spring Web Flow or JSF navigation rules in this example.

Spring-managed JSF Controller Beans

For this solution, there are two Spring-managed JSF controller beans of interest: example.jsf.GuestSupport and example.jsf.UserSession. Here is the definition from the example-jsf.xml file:

<bean id="guest" class="example.jsf.GuestSupport" scope="request">
<property name="userService" ref="userService"/>
<property name="facesSupport" ref="facesSupport"/>
</bean>

The GuestSupport bean (request-scoped) is the controller for actions performed by guests, such as login and register. Spring creates a new instance per request (if needed).

<bean id="userSession" class="example.jsf.UserSession" scope="session">
<property name="userService" ref="userService"/>
<property name="facesSupport" ref="facesSupport"/>
</bean>

The UserSession bean (session-scoped) is the controller for actions performed by authenticated users, such as managing preferences. There will be one instance of UserSession per HttpSession; Spring makes sure a new UserSession bean is created whenever a new HttpSession is created, which happens before the user is logged in. Thus, there are two possible states for the UserSession bean:

  1. User has not been authenticated ( remoteUser == null )
  2. User has logged in ( remoteUser != null )

Thus, you need to make sure not to use this bean in your JSF EL unless the user is logged in.

Registration

When a user clicks on the Register link on the home page, a RichFaces <rich:componentcontrol> is used to open the RichFaces ModalPanel with id registerPanel.

<rich:componentControl for="registerPanel" attachTo="registerLink" operation="show" event="onclick"/>

The form is bound to the doRegister method on our GuestSupport bean using a standard JSF commandButton. Under the covers, JSF uses the SpringBeanELResolver to get an instance of the GuestSupport bean from Spring. This is configured in faces-config.xml:

<application>
  <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
</application>

Registration requires the user to solve a CAPTCHA. The easiest way to add this to your site is to use reCAPTCHA. The best way to get started with reCAPTCHA is to read their developer's guide at http://recaptcha.net/whyrecaptcha.html. I had to use a custom JSF tag to embed reCAPTCHA JavaScript into the registration form because the standard JSF script tag was being written to the page header instead of inline. This is due to the following setting in the web.xml:

<context-param>
  <param-name>com.sun.faces.externalizeJavaScript</param-name>
  <param-value>true</param-value>
</context-param>

This allows browsers to cache our JSF JavaScript instead of having to write it each time the page is accessed, which presumably will help our pages load faster. However, we need the reCAPTCHA script to be inline and not in the header. Fortunately, JSF makes it very easy to create custom tags.

<example:script src="#{uiSupport.recaptchaScriptUrl}">
Strong Password Validation

Notice that the registration form binds the password to the guest.passwordSupport.password.

<h:inputSecret label="#{i18n.login_password}" id="password" required="true" value="#{guest.passwordSupport.password}" size="12" maxlength="12">
  <rich:beanvalidator/>
</h:inputSecret>

This is because the User password is expected to be a one-way MD5 hash instead of plain text. The example.user.PasswordSupport utility bean validates the password using Hibernate Validator:

@NotEmpty
@Length(min=8,max=12)
@Pattern(regex="(?=^.{8,}$)((?=.*\\d)|(?=.*\\W+))(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$", message="Password is not secure enough!")
private String password = null;

Upon successful registration, the guest.newUserName and guest.newUserEmail properties are updated on the guest bean. This activates the regPending modal panel to notify the user that their registration must be activated by clicking on an activation link sent to their email. However, if registration fails, then the registerPanel is displayed with the appropriate error message(s).

<a:commandButton id="registerButton" value="#{i18n.register}"
    action="#{guest.doRegister}" styleClass="rich-fileupload-button" reRender="regPending"
    oncomplete="if (document.getElementById('jsfMsgMaxSev').value != '2') { #{rich:component('registerPanel')}.hide(); #{rich:component('regPending')}.show(); }"/>

The oncomplete attribute will close the registerPanel and open the regPending panel if no errors are present in the AJAX response. This relies on the use of an A4J <a:outputPanel>, see index.jsp.

Account Activation

The activation link includes the act parameter containing a hex-encoded MD5 hash code linked to the pending registration. The guest.getUser() method checks for the act parameter and if present tries to activate the user. If successful, the login panel is displayed with the new user name pre-popluated.

Login

For authentication, we delegate to Spring Security using an ExternalContext dispatch to /j_spring_security_check. This handler expects two parameters provided by our Login form: j_username and j_password. Notice that the login form is configured with prependId="false" so that JSF does not add loginForm: to the parameter names. The request to /j_spring_security_check is handled by the springSecurityFilterChain configured in web.xml:

<filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

Spring Security, configured in example-security.xml, uses a userDetailsService based authentication provider. In this case, we configure two simple SQL queries based on the Hibernate schema for the org.springframework.security.userdetails.jdbc.JdbcDaoImpl.

Authenticate by Username and Password:

SELECT userName, password, active from ex_user where userName=?

Get Roles for User:

SELECT ij.userId, r.name FROM ex_role r INNER JOIN (SELECT u.userId, ur.roleId FROM ex_user_role ur, ex_user u WHERE ur.userId=u.userId AND u.userName=?) ij ON r.roleId=ij.roleId

If authentication fails, Spring Security puts the exception into the session keyed by: AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY. In the JSF layer, we need to check for this exception and if present display a generic JSF error message to the user. It's a good idea to show a generic message because the actual exception may contain information a hacker can use to break into the site. Unfortunately, I had to resort to invoking a static function GuestSupport.setLoginPanelVisibility from a JSP Scriptlet in auth.jspf to check for the Spring Security exception.

Remember Me

Recall that one of our requirements was to support a Remember Me feature so that returning users are automatically logged into the site. With Spring Security, this is trivial; the JSF login form simply needs to pass the _spring_security_remember_me parameter. Under the covers, Spring Security is configured to use the simple hash-based token approach. However, you can also use a more secure solution described here.

JavaScript and AJAX Tricks

In this section, I demonstrate a few of the JavaScript and AJAX tricks used by the solution.

Country / Region

One of the nice things about RichFaces is that you can attach AJAX-driven logic to any JSF component without writing any JavaScript. For example, suppose we want to limit the list of regions based on the selected country on the Preferences form. This is trivial using the <a4j:support> tag provided by RichFaces:

<a4j:support event="onchange" rerender="region" ajaxsingle="true">

This fires an AJAX request when the country is changed and upon success, the region component is re-rendered with a country-specific list of regions.

User name check with AJAX

During registration, if the user's desired screen name is already taken by another user, we should let them know immediately. To do this, we use the <a4j:support> tag provided by RichFaces on the userName <h:inputText> field:

<a4j:support event="onblur" rerender="userName" ajaxsingle="true">

When the onblur event occurs, RichFaces sends an AJAX request to update the guest.registrationScreenName property. If the desired screen name is already taken, a FacesMessage is created to be displayed by the <rich:message> tag associated with the userName field.

<rich:message for="userName">
  <f:facet name="errorMarker"><h:graphicimage value="img/error.gif"></f:facet>
</rich:message>

29 comments:

  1. Hi!
    Its a really great example and I am definetly going to use it as a template for my coming projects.

    The RECAPTCHA AUTHENTICATION part of the code is not rendered/not visible in the site. Do you know what could be wrong?

    thanks for the great tutorial

    ReplyDelete
  2. Is it possible that you have not defined a public key for the RECAPTCHA AUTHENTICATION?

    regards

    ReplyDelete
  3. Just a reminder that you need to register with the reCAPTCHA site to get your own private / public keys. Once you have those, then you need to plug them in the config/example-jsf.xml file ... look for:

    <property name="recaptchaPrivateKey"
    value="GET YOUR OWN PRIVATE KEY"/>
    <property name="recaptchaVerifyUrl"
    value="http://api-verify.recaptcha.net/verify"/>
    <property name="recaptchaScriptUrl"
    value="http://api.recaptcha.net/challenge?k=GET YOUR OWN PUBLIC KEY"/>

    You can register with reCAPTCHA to get your own keys at http://recaptcha.net/whyrecaptcha.html.

    If you've done this, then try viewing the source of the page from your browser to see what my custom tag is putting out. Feel free to post back here with more information.

    ReplyDelete
  4. Hi!
    Thank you for your prompt reply. Now the recaptcha part works But it fails to send mail. I get the following error:

    org.springframework.mail.mailsendexception: mail server connection failed; nested exception is
    javax.mail.MessageException: Could not connect to SMTP host: example.com port:25;

    Do you have any idea, where the mistake is?

    ReplyDelete
  5. I solved the problem regarding sending mail. I edited example-env.properties to use gmail and it all went well.

    An observation, when the mail is send, the activationURL is localhost:8989 instead of localhost:8080

    Thank you for the great tutorial.

    ReplyDelete
  6. Glad to hear you have the reCAPTCHA stuff working ...

    Re: localhost:8989 ...

    You need to change the 'activationLinkUrl' property in example-env.properties to match your server.

    ReplyDelete
  7. Hi,

    I made these changes in examples-env.properties file.
    mailhost=gmail.com
    But I am still getting an error saying that "Could not connect to SMTP hose gmail.com port 25".

    Do you know why it is so?
    Is there any way, I could disable this mail sending functionality?

    ReplyDelete
  8. gmail.com is probably not a SMTP server ... The only way to disable email at this point would be to alter the logic in the UserServiceImpl::createUser method but that sort of defeats the purpose of this solution.

    ReplyDelete
  9. Instead of gmail.com, could uo suggest any other SMTP server I can use to test or have a look at the solution?

    ReplyDelete
  10. Hello out there!

    I'm using the same Spring versions like the Example, but I always gets the error:

    "lang.NoSuchMethodError: org.springframework.web.context.ConfigurableWebApplicationContext.setConfigLocation(Ljava/lang/String;)V"

    I'm getting crazy and really don't knwo what I can still do....

    I don't user maven, so i cannot exclude dependencys or such things!

    Can anybody help me please, i'm really desperate!

    I also tried a lot of other versions of spring.

    2.5.5, 2.5.1 .. all those!

    Can anyone with a running application maybe post his jar archives, what can i do else?

    ReplyDelete
  11. Learn Wicket. Forget JSF.

    ReplyDelete
  12. Hi!

    I've been trying to implement solution similar to yours for the login panel.

    My big problem is that apparently when I execute the dispatch (or somewhere around that) the session gets reset.

    This leads me to 2 big problems:
    1) every time a login is successful, I just fall back to the same login page again and again..
    2) if I check the remember me I got an exception regarding the session not being there anymore (I'll provide you with the full trace if you want). But then when I reload the page I'm logged in as if it worked.

    From the logs in spring security it appears that the session ID changes every time I authenticate. No problems if I fall back in the standard login form.

    I have the feeling that this could be a simple configuration issue (one of those you bang your head on for days and then just feel stupid for not having seen it before), has it ever happen to you something similar?

    Thanks in advance.

    ReplyDelete
  13. http://sensiblerationalization.blogspot.com/2009/07/from-jsf-to-richfaces-to-richfaces.html for those who would like to get stared with Richfaces

    ReplyDelete
  14. For those who know JSF and would like to get started with Richfaces and probably portal, I wrote an intro blog at http://sensiblerationalization.blogspot.com/

    ReplyDelete
  15. Is there a complete soucre code?

    ReplyDelete
  16. Can anyone please tell me if I want to use Oracle in place of hsqldb what should i do?

    ReplyDelete
  17. Hi,
    Great article! I'm starting using Spring MVC, so i would like to know why use the Service Layer based on SpringTransactionProxy instead of Control Layer (from SpringMVC) to leads the business logic. How i can modify it for Spring MVC ? Which gains and losses ?

    ReplyDelete
  18. Hello all,
    im facing a problem trying to execute this sample. After press the REGISTER button on Register page my application was through an exception. Its strange because i got the CAPTCHA text correctly and after enter the text in the textbox i receive the message [Incorrect CAPTCHA text, please try again] followed by exception message error below in TOMCAT console.

    java.net.SocketTimeoutException: connect timed out
    at java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:333)
    at java.net.PlainSocketImpl.connectToAddress(PlainSocketImpl.java:195)
    at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:182)
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:366)
    at java.net.Socket.connect(Socket.java:519)
    at sun.net.NetworkClient.doConnect(NetworkClient.java:158)
    at sun.net.www.http.HttpClient.openServer(HttpClient.java:394)
    at sun.net.www.http.HttpClient.openServer
    :
    sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLCo
    nnection.java:896)
    at example.jsf.FacesSupport.post(FacesSupport.java:165)
    at example.jsf.FacesSupport.isCaptchaSolved(FacesSupport.java:147)
    at example.jsf.GuestSupport.doRegister(GuestSupport.java:157)
    at


    Anyone any suggestion ?
    Thanks in Advance

    ReplyDelete
  19. Hi,
    someone can say me how the getRecaptchaVerifyUrl() method recover the URL string or where recaptchaVerifyUrl variable is setted ? I didn't find any references for setRecaptchaVerifyUrl() method and i'm facing a problem related to connection in Post method.

    Thanks in advance.

    ReplyDelete
  20. recaptchaVerifyUrl is set in example-jsf.xml

    ReplyDelete
  21. Hi,
    why i receive a SocketTimeoutException in Tomcat Console when i submit the CAPTCHA text from Register Form ?
    Thanks

    ReplyDelete
  22. thank you very much for your sharing. it was so useful in our project.It works well. thanks alot again:D

    ReplyDelete
  23. hallo, pls can enyone tell me how to use MySQL instead of hsql?..pls this is very urgent to me..

    ReplyDelete
  24. An excelent article. I think that you have written this article from your own experience in development. I have read many articles, but i never have seen an article about authentication fullest and full explained.
    I was impressed with the whole of knowledge applied here, then I would like you to recommend me some references (books, articles) to complement it based in your experience.

    ReplyDelete
  25. Dear Friends. I downloaded the source code. Tomcat works perfect, but when I put the example.war at webapps and test it on my browser -> http://localhost:8080/example/ I´m getting a 404 Tomcat´s error. Anyone knows what can be that issue? Thank you so much.

    ReplyDelete
  26. The source can not be downloaded :-(

    ReplyDelete
  27. Really An excellent article.
    but i can't download the source code.
    could u upload it again or tell me how to get it plz.

    ReplyDelete
  28. Trying to find a new home for the source code for the Spring project as some bot got ahold of the URL and drove my bandwidth usage over the limit.

    Please send an email to thelabdude at gmail and I'll send you a link to the temporary download location.

    ReplyDelete