In this article, I show you how to implement a Web 2.0 style user registration solution using JBoss Seam with JSF/Facelets/RichFaces and Hibernate. This article evolved from a real Web 2.0 site I'm building. 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 plan to incorporate Hibernate Search and some 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 JSF and Hibernate. However, I do spend extra time explaining some of the key features of Seam and RichFaces.
I'd like to give credit to Dan Allen as I've used many of the ideas he teaches in Seam In Action in my own Seam development; his book is one that all Seam developers should own!
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).
Jump to solution ...
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.
Jump to solution ...
3. Remember Me
Registered users can request the site to remember their credentials for automatic login for future visits.
Jump to solution ...
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.
Jump to solution ...
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.
Jump to solution ...
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.
Jump to solution ...
Seam-based Architecture
In a previous blog posting, I used Spring with JSF / RichFaces to satisfy these requirements and advocated a layered architecture. While that is still a valid approach, I take a different approach in this article to highlight key features of Seam. Specifically, instead of covering each layer, I describe how I implemented each requirement. The layers are there if you need them, but with Seam you're not forced to think in terms of stateless layers. What initially attracted me to Seam was its get down to business approach. First off, it comes with a tool seam-gen which creates the skeleton project for your application, including generating JPA-based Entity code by reverse engineering a database schema. With Seam, you can get right to work building your UI that directly interacts with your entities. In contrast, with my Spring-based solution, I had to flush out the persistence and business-service layers consisting of the following interfaces and classes:
example.user.UserService (interface)
example.user.impl.UserServiceImpl (class)
example.user.dao.UserDao (interface)
example.user.dao.RoleDao (interface)
example.user.dao.impl.HibernateUserDao (class)
example.user.dao.impl.HibernateRoleDao (class)
Seam, on the other hand, treats the persistence manager as the DAO and provides a framework for CRUD components (see Seam Application Framework). I'll address the Seam Application Framework in a future article, for now I'm going to stay focused on the requirements at hand. I just wanted to make sure you knew that I wasn't throwing the persistence and business-service layers out the window by using Seam. They are there when you need them but are not in the way otherwise.
Here are some of the key aspects of Seam that I'll be using to satisfy the requirements:
- POJOs with bi-directional dependency injection known as bijection (note: I'm not using EJB3 for this project, but Seam makes it really easy to use EJBs if you need them)
- Object-Relational Mapping (ORM) using the Java Persistence API (JPA) and Hibernate under the covers.
- Declarative transaction management using Seam annotations
- JSF with Facelets and RichFaces
- Test-driven development based on TestNG
Seam Project Organization
Here is the link to the Seam project created by seam-gen. Before digging too deeply into the details of the configuration, take moment to familiarize yourself with the organization of the Seam project:
NOTE: You'll need to copy the lib directory from your Seam 2.1.1.GA directory to my project directory as I excluded the lib directory to minimize the size of the download.
Object Persistence via JPA
Domain Entities
There are three primary domain entities: User, Preferences, and Role, which should be self-explanatory given the requirements outlined above. The entities are POJOs with the exception of using Seam, JPA, and Hibernate annotations in a few key places. For example, in the following code snippet, we annotate the example.user.User class as a JPA Entity and map it to the ex_user table in the database:
@Entity
@Table(name = "ex_user", uniqueConstraints = @UniqueConstraint(columnNames = "user_name"))
public class User implements Serializable ...
Notice that I'm using surrogate keys for my domain object identifiers, which are auto-generated by Hibernate using the best approach for the specific database-type:
@Id @GeneratedValue(generator="native")
@GenericGenerator(name="native", strategy = "native")
@Column(name = "user_id", unique = true, nullable = false)
public Long getUserId() {
return userId;
}
I'm also using the Hibernate Validator annotations to restrict the values for some of the members, such as the user's email address:
@Column(name = "email", nullable = false, length = 60)
@NotNull
@Length(max = 60)
@Email
public String getEmail() {
return this.email;
}
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 this.preferences;
}
There is a bidirectional many-to-many relationship between the User and Role objects using a join table ex_user_role.
@UserRoles
@ManyToMany(targetEntity = Role.class,cascade = {CascadeType.PERSIST,CascadeType.MERGE})
@JoinTable(name = "ex_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
public Set getUserRoles() {
if (userRoles == null) {
userRoles = new HashSet ();
}
return userRoles;
}
On the Role object, we have:
@ManyToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE},mappedBy="userRoles",targetEntity=User.class)
public Set getRoleUsers() {
if (roleUsers == null) {
roleUsers=new HashSet ();
}
return roleUsers;
}
This solution comes directly from the Hibernate documentation, section 7.5.3.
Notice that my entity classes are in the src/main directory as these classes cannot be incrementally hot-deployed by Seam. This means that if you change the code in these classes, then you need to recycle the Web application before the change is activated. On the other hand, classes in the src/hot directory are updated by Seam on-the-fly without recycling the Web application. Consequently, you want to put as many of your classes in the src/hot directory as possible (see: 2.8. Seam and incremental hot deployment).
Now that you understand the key domain entities involved in this solution, you're ready to see how I implemented each requirement.
Solution for Requirement #1 (Public-facing Web site with read-only access to Guests)
Much like many Web 2.0 sites today, my site will be useful to casual guests of the site. So it needs to load quickly and look attractive on all major browsers (IE 6-7, FireFox 3.x, and Safari 3.x). Thankfully, JSF and RichFaces satisfy these requirements out-of-the-box. Here are some of the reasons why I like JSF with RichFaces:
- Modern, professional looking skin out-of-the-box (I don't like 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.
- Templates using Facelets
Key design decision: I decided to use the RichFaces ModalPanel component for my forms 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 JSF navigation rules in this example because there is only one page.
Solution for Requirement #2 (Open registration process with email verification)
For this requirement, it's useful to break it down into several detailed steps:
- Guests are presented with a link to register.
- Simple form to collect minimal user information.
- Require the user to solve a CAPTCHA to prevent bots from creating accounts.
- Require the user to agree to the site's terms of use.
- Validate the user's email address before activating the account.
- Activate the account when the user clicks on a link in the activation email.
Solution for Requirement #2a (Register Link)
In the view/WEB-INF/facelets/headerControls.xhtml file, I open the registerPanel dialog using a rich:componentControl which calls the show method of the rich:modalPanel component when the link is clicked:
<h:outputLink value="#" id="registerLink">
<h:outputText value="#{i18n.register}"/>
<rich:componentControl for="registerPanel" attachTo="registerLink" operation="show" event="onclick"/>
</h:outputLink>
Solution for Requirement #2b (Simple form to collect minimal user information)
In the view/WEB-INF/facelets/guestSupport.xhtml file, I use a rich:modalPanel to hold the registration form. My registration form only requires the user to supply a screen name, password, email, first, and last name. Notice that the form fields are bound to properties of a guest component, for example:
<h:inputText label="#{i18n.login_username}" id="userName" required="true"
value="#{guest.registrationScreenName}" size="12" maxlength="12" redisplay="true">
...
</h:inputText>
The guest object is a Seam component example.action.GuestSupport that handles the registration process, see: src/hot/example/action/GuestSupport.java. Notice the annotations on the GuestSupport class:
@Name("guest")
@Scope(ScopeType.EVENT)
public class GuestSupport ...
The @Name annotation means that this is a Seam component that can be referenced in the EL using the identifier guest, such as: #{guest.registrationScreenName}. The @Scope(ScopeType.EVENT) annotation means that the component is managed in Seam's EVENT scope, which means the component exists from the Restore View phase until the end of the Render Response phase in the JSF lifecyle. Essentially, the guest component exists only to handle a guest action and then is released by the Seam container. If no guest actions are performed, then this component is never created.
The form is bound to the guest.doRegister method:
<a:commandButton type="button" id="registerButton" value="#{i18n.register}" action="#{guest.doRegister}"/>
Notice that I'm using a RichFaces AJAX form <a:form> which means the register action will be submitted as an AJAX request without reloading the entire page.
Solution for Requirement #2c (CAPTCHA)
In my Spring solution, I integrated with reCAPTCHA. However, Seam comes with a CAPTCHA solution built-in so I decided to give it a try. Here is how to embed the Seam CAPTCHA solution into your form:
<s:decorate id="verifyCaptchaField" template="captcha.xhtml">
<h:graphicImage id="captchaChallenge" value="/seam/resource/captcha" styleClass="captchaChallenge"/>
<h:inputText id="verifyCaptcha" value="#{captcha.response}" required="true" size="3"/>
</s:decorate>
The default solution is to show an image of a simple addition problem, which unlike reCAPTCHA is much easier to read and is probably pretty safe unless your site is for preschoolers who might not be able to solve the addition problem.
From what I've seen, it's also pretty easy to extend the built-in solution to provide your own image creation solution. You should note that if you use Seam's CAPTCHA solution, then you must make sure the Seam Resource Servlet is activated in your web.xml file (done automatically by seam-gen).
Solution for Requirement #2d (Terms of Use)
I built a placeholder HTML file to hold the site's legal text, see: view/WEB-INF/facelets/legal.html. If the user submits the form without agreeing to the terms of use, then a nice error message is returned. This is handled by the following code in my doRegister method:
if (!agreedToTermsOfUse) {
String msg = facesSupport.getMessage("please_agree_to_terms", extCtxt.getRequestLocale());
jsf.addMessage("registerForm:registerButton", new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, null));
return;
}
Solution for Requirement #2e (Email Verification)
The doRegister method creates the user but does not activate the account. Instead, it creates an activation key which is an MD5 hash of the user's information and the current timestamp. The timestamp is needed because there is a very slight chance of getting the same hash code for two different users. The activation key is sent to the user as a link back to the site that will activate their account.
Seam uses Facelet templates as email templates. For account verification, I created the view/WEB-INF/facelets/email/activation.xhtml template. Notice that I can use the EL to reference my Seam components just as I would for an HTML template:
<m:from name="JSF / RF Seam Example" address="support@saastk.com"/>
<m:to name="#{inactiveNewUser.email}">#{inactiveNewUser.email}</m:to>
<m:subject>JSF / RF Seam Example Account Activation</m:subject>
<m:body>
<html>
<body>
<p>Dear #{inactiveNewUser.displayName},</p>
<p>Please click on the following link to activate your account:</p>
<p><a href="#{inactiveNewUser.activationLink}">#{inactiveNewUser.activationLink}</a></p>
</body>
</html>
</m:body>
The registrationMailer component processes the email templates and sends the email, see: src/hot/example/action/RegistrationMailer.java. Notice the annotations on the RegistrationMailer class:
@Name("registrationMailer")
@AutoCreate
@Scope(ScopeType.APPLICATION)
public class RegistrationMailer ...
The @Name annotation means that this is a Seam component that can be referenced in the EL using the identifier registrationMailer. The @Scope(ScopeType.APPLICATION) annotation means that the component is managed in Seam's APPLICATION scope, which means the component exists while the Web application is active. Essentially, there will be only one registrationMailer used by the Seam container. @AutoCreate means that Seam will create this component for us automatically.
I decided to send the email as part of the transaction meaning that if the email fails, then no account is created. The following code puts an instance of the aptly named InactiveNewUser bean into EVENT scope and the raises a synchronous application event.
Contexts.getEventContext().set("inactiveNewUser",
new InactiveNewUser(newUser.toString(), newUser.getUserName(), newUser.getEmail(), newUserLink));
Events.instance().raiseEvent("userRegistered");
The RegistrationMailer is an observer of this event:
@Observer("userRegistered")
public void sendActivationEmail() {
renderer.render("/WEB-INF/facelets/email/activation.xhtml");
}
Supposedly, you can send the email asynchronously, but I ran into trouble so I stayed with the synchronous approach for now. I'll post to the Seam forum to see what I can find out ...
NOTE: Be sure to configure a <mail:mail-session host="???" port="25"/> in the Seam configuration file: resources/WEB-INF/components.xml. See Chapter 21. Email
Solution for Requirement #2f (Account Activation)
Assuming all goes well and the email provided by the user is valid, then they will receive an email with a link back to the site, such as:
http://localhost:8989/jsf_rf_seam_user_reg/home.seam?act=19eb2e7567913c47a931e305cdeb6d311242247928343
When the user clicks on the link, we need to process the act request parameter. Fortunately, Seam makes this really easy to do using a page action. In resources/WEB-INF/pages.xml I declare an action for the home.xhtml page:
<action execute="#{guest.doActivate(request.getParameter('act'))}"
if="#{request.getParameter('act') != null}"/>
In plain English, Seam invokes the guest.doActivate method if the request contains the act parameter. If activation is successful, then I trigger the Login form to open up with the newly activated username pre-populated.
@Transactional
public void doActivate(String activationKey) {
Query q = entityManager.createQuery("from User u where u.active=0 AND u.activationKey=:activationKey");
q.setParameter("activationKey", activationKey);
User activatedUser = (User)q.getSingleResult();
activatedUser.setActive(true);
activatedUser.setActivationKey(null);
loginUserId = activatedUser.getUserName();
credentials.setUsername(loginUserId);
log.info("User {0} activated successfully.", loginUserId);
}
NOTE: You may want to have a background process that cleans up accounts that haven't been activated after so many days.
Solution for Requirement #3 (Remember Me)
Login
Before I can tackle the Remember Me requirement, I need to make sure you know how login works with Seam. If you search my code, you'll see that there is no login method. Instead, I rely on Seam's Identity Management framework to do the login work for me. The Identity Management framework requires an identity store in resources/WEB-INF/components.xml:
<security:jpa-identity-store user-class="example.user.User" role-class="example.user.Role"/>
Notice that I'm reusing my User and Role Entities that I've already developed. My login form is bound to Seam's identity.login method:
<h:commandButton type="submit" id="loginBtn" action="#{identity.login}" value="#{i18n.login}"/>
See loginPanel in view/WEB-INF/facelets/guestSupport.xhtml
If login is successful, then Seam raises the Identity.EVENT_LOGIN_SUCCESSFUL event; if login fails, then Seam raises the Identity.EVENT_LOGIN_FAILED event. Thus, my GuestSupport component is configured as an @Observer for these events:
@Observer(Identity.EVENT_LOGIN_SUCCESSFUL)
public void onLogin() {
this.currentUser = byUserName(credentials.getUsername());
log.info("User {0} logged in successfully.", this.currentUser.getUserName());
}
@Observer(Identity.EVENT_LOGIN_FAILED)
public void onLoginFailed() {
this.currentUser = null;
FacesContext jsf = FacesContext.getCurrentInstance();
String msg = facesSupport.getMessage("login_error", jsf.getExternalContext().getRequestLocale());
jsf.addMessage("loginForm:loginBtn", new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, null));
}
In the onLogin method, we see an application of Seam's outjection @Out in action. If login succeeds, then we outject the currentUser object into Seam's SESSION Scope as seen by:
@Out(required=false, scope=ScopeType.SESSION)
private User currentUser;
So, unlike Spring, Seam can easily export runtime objects into the container using simple annotations! Once outjected, the currentUser component can be used seamlessly in our JSF UI.
At this point, you may be wondering how Seam verifies the credentials supplied by the user against the database. There is a little annotation magic going on here that is not immediately apparent. If you study the example.user.User class, you'll see that I'm using a few annotations from the org.jboss.seam.annotations.security.management package:
@Column(name = "user_name",unique = true,nullable = false,length = 16)
@NotNull
@Length(min = 4,max = 16)
@Pattern(regex = "^[a-zA-Z\\d_]{4,12}$",message = "Invalid screen name.")
@UserPrincipal
public String getUserName() {
return this.userName;
}
...
@Column(name = "password",nullable = false,length = 128)
@NotNull
@Length(max = 128)
@UserPassword(hash = "SHA")
public String getPasswordHash() {
return this.passwordHash;
}
The @UserPrincipal and @UserPassword annotations are used by the Seam Identity Management framework to verify the credentials during login. With these annotations, Seam has all the information it needs to validate credentials against a one-way SHA-1 hash stored in the database.
Remember Me
Seam supports two approaches to remembering users when they return to your site, aptly named Remember Me: 1) only the username is remembered, or 2) a persistent access token is stored in a cookie to allow full automatic login. If you read the Seam documentation, you are strongly advised against the latter approach due to cross-site scripting vulnerabilities. While I understand those concerns, it is still a really nice feature to offer for users. Consequently, I'm going to show you how I implemented the persistent token approach and if you don't decide to use it, then it is very easy to rollback to the username only solution. I actually think the username-only solution is pretty slick if implemented correctly. LinkedIn is a good example of the username only solution where it recognizes you and shows your profile in read-only mode. LinkedIn only requires you to login if you try to make a change to your profile.
There are four steps you need to do to enable automatic login:
First, you need to activate Seam's rememberMe component in the resources/WEB-INF/components.xml file and set the mode to autoLogin:
<security:jpa-token-store token-class="example.security.AutoLoginToken"/>
<security:remember-me mode="autoLogin"/>
The rememberMe component needs a token store. Seam provides a JPA-based implementation org.jboss.seam.security.JpaTokenStore that stores the token in your database, but you need to provide the Entity class, see src/main/example/security/AutoLoginToken.java. The AutoLoginToken implementation came directly from 15.3.5.1. Token-based Remember-me Authentication in the Seam documentation.
Second, you need to add a checkbox on the login form bound to the rememberMe component's enabled property:
<h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
Third, you need to invoke the identity.tryLogin method when the home page is requested. As with the activation link processing, I configured a simple page action for home.xhtml in pages.xml:
<action execute="#{identity.tryLogin}" if="#{not identity.loggedIn}"/>
The Seam documentation mentioned using the following event declarations in components.xml, but that did not work for me:
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
<action execute="#{identity.tryLogin()}"/>
</event>
<event type="org.jboss.seam.security.loginSuccessful">
<action execute="#{redirect.returnToCapturedView}"/>
</event>
Lastly, you need to observe the Identity.EVENT_POST_AUTHENTICATE event and out-ject the currentUser component into Session scope if auto-login was successful:
@Observer(Identity.EVENT_POST_AUTHENTICATE)
public void postAuthenticate(Identity identity) {
if (identity != null && identity.isLoggedIn() && this.currentUser == null) {
String userName = identity.getUsername();
if (userName == null) userName = identity.getPrincipal().getName();
if (userName != null) {
this.currentUser = byUserName(userName);
log.info("User {0} logged in successfully.", this.currentUser.getUserName());
}
}
}
The Identity.EVENT_LOGIN_SUCCESSFUL event was not raised by the rememberMe component upon successful auto-login, which may be a bug? I'll ask the Seam folks ... for now, you have to have this additional @Observer in place :-(
Solution for Requirement #4 (Ability to recover forgotten passwords)
If a user forgets their password, then they can request the site to email a temporary password by submitting their screen name and email.
Since this is an action performed by a guest of the site, the form is processed by the guest component:
@Transactional
public void doRecoverLostPassword() throws Exception {
...
}
The @Transactional annotation tells Seam that this method must be invoked within the context of a transaction. If the method completes successfully, then Seam will commit the work for you regardless of the underlying transaction manager your application is using.
When the user logs into the site with their temporary password (sent in an email), the site needs to prompt them to change the password to a permanent value. As before, to keep the user's orientation with the site, I pop-up a modal dialog to require the user to change their password, see chngPswdPanel in view/WEB-INF/facelets/userPreferences.xhtml.
The modal panel will only show itself if the user's password is temporary:
<rich:modalPanel id="chngPswdPanel" width="320" height="240" rendered="#{identity.loggedIn}" showWhenRendered="#{currentUser.temporaryPassword}">
The change password form is handled by the changePassword component, see: src/hot/example/action/ChangePasswordAction.java
Solution for Requirement #5 (User Preferences)
Once logged in, the user can click on the Preferences link to edit their profile and control how they want the site to interact with them. The prefPanel dialog in userPreferences.xhtml is activated using the following AJAX Link a:commandLink in headerControls.xhtml:
<a:commandLink id="prefLink" action="#{updatePreferences.beginEditPreferences(currentUser.userId)}" oncomplete="#{rich:component('prefPanel')}.show()" value="#{i18n.preferences}"/>
Look closely at the EL for the action attribute; I'm using a new component updatePreferences, see: src/hot/example/action/UpdatePreferencesAction.java.
@Name("updatePreferences")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{identity.loggedIn}")
@Transactional
public class UpdatePreferencesAction implements java.io.Serializable ...
The Scope of this component is ScopeType.CONVERSATION, which means this component will exist for the duration of a conversation, possibly spanning multiple requests from the user. The Seam documentation equates a conversation with a single unit-of-work from the perspective of the user (not the database). In this case, the unit-of-work is to edit preferences, which may include uploading a photo as well as submitting an AJAX form one or more times. So the conversation begins when the user opens the preferences dialog and ends when they close it. Here is the beginEditPreferences method:
@Begin(flushMode = FlushModeType.MANUAL, join = true)
public void beginEditPreferences(Long userId) {
currentUser = entityManager.find(User.class, userId); // attach to this EntityManager
log.info("User {0} has started editing preferences.", currentUser.getUserName());
}
When the conversation begins, I use the entityManager to get the User Entity for the current user, identified by the userId parameter. Of course, there is a currentUser component in Session Scope (out-jected during the login process). However, it is not attached to the updatePreferences component's entityManager (it is a detached Entity that is only useful for reading data about the current user). When the beginEditPreferences method completes, the currentUser stays attached to the entityManager for the duration of the conversation.
While the preferences dialog is open, the user can submit several requests to the server, such as uploading an image. For image upload, I employ the RichFaces rich:fileUpload component:
<rich:fileUpload fileUploadListener="#{updatePreferences.fileUploadListener}" maxFilesQuantity="1" id="uploadPhoto" immediateUpload="true" acceptedTypes="jpg,gif,png" allowFlash="false" listHeight="110">
<a:support event="onuploadcomplete" reRender="prefPanel,uploadPhotoPanel"/>
</rich:fileUpload>
The fileUploadListener method participates in the conversation accessing the currentUser that was attached to the entityManager when the conversation began.
When the user closes the preferences dialog, the conversation ends by invoking an AJAX call to updatePreferences.endEditPreferences which is annotated with @End:
@End
public void endEditPreferences() {
entityManager.flush(); // flush the changes to the database
log.info("User {0} finished editing preferences.", currentUser.getUserName());
}
Notice that the updatePreferences component out-jects the currentUser component back into Session scope (as the guest component did after a successful login). This is needed so that changes are propagated back to the UI, such as the user's display name shown in the top right corner of the application.
It may be hard to appreciate Seam conversations with such simplistic objects at play. However, as your object associations become more complex, conversations are immensely powerful in helping you manage updates to your entities.
Solution for Requirement #6 (Strong Server-side Validation using AJAX)
With Seam, you don't need any JavaScript validation--all validation can be done efficiently on the server. For example, notice how I ensure the user's password is strong:
<h:inputSecret label="#{i18n.login_password}" id="password" required="true" value="#{passwordSupport.password}" size="12" maxlength="12">
<f:validateLength minimum="8" maximum="12"/>
<rich:beanValidator/>
</h:inputSecret>
On the server, the password is validated against a regular expression 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;
See: src/hot/example/action/PasswordSupport.java
Most of the forms used in this solution are submitted using AJAX requests. The login form and the image upload form are the only two forms that redisplay the entire page when submitted. Here are some other areas where I've used the AJAX features of RichFaces:
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:
<h:inputText label="#{i18n.login_username}" id="userName" required="true" value="#{guest.registrationScreenName}" size="12" maxlength="12" redisplay="true">
<f:validateLength minimum="4" maximum="12"/>
<rich:beanValidator summary="#{i18n.invalid_screen_name}"/>
<a:support event="onblur" reRender="userName" ajaxSingle="true"/>
</h:inputText>
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>
Test Driven Development with Seam
One of the major attractions of POJO development and frameworks like Spring is the ease in which you can test your code as you develop it outside the container. Seam offers a similar solution, but does require the JBoss micro-container to execute tests. Other than the tests running much slower than similar Spring-based tests, I found testing with Seam to be very easy and effective. On the other hand, I wasn't able to find a simple JSF testing solution for Spring anyway, so just having the SeamTest framework in place is a benefit over Spring, even if it is slower. Seam tests are executed by TestNG. I've implemented a test that performs all the actions needed to support the requirements described above, see: src/test/example/test/UserRegistrationTest.java. Notice that you use the same EL as your JSF Facelets do!
Lastly, I want to mention one thing you should to do to fix an issue with incremental hot deploy. The seam explode command is supposed to hot-deploy your Facelets and Java classes from the hot directory. However, it also updates the JCA ConnectionFactory in the JBoss deploy directory. This can cause issues (see: https://jira.jboss.org/jira/browse/JBSEAM-3844?focusedCommentId=12442804). My solution was to add a new target to my project's build.xml file hotdeploy, which is the same as the explode target except datasource is no longer a dependency:
<target name="hotdeploy" depends="stage" description="Deploy the exploded archive but not the datasource">
<fail unless="jboss.home">
jboss.home not set
</fail>
<mkdir dir="${war.deploy.dir}"/>
<copy todir="${war.deploy.dir}">
<fileset dir="${war.dir}"/>
</copy>
</target>
You also have to update the build.xml file in the seam-gen directory of your Seam distribution:
<target name="hotdeploy" depends="validate-project" description="Deploy the project as an exploded directory">
<echo message="Deploying project '${project.name}' to JBoss AS as an exploded directory"/>
<ant antfile="${project.home}/build.xml" target="hotdeploy" inheritall="false"/>
</target>
IMPORTANT: I excluded the lib directory from the download zip to reduce the file size. You simply need to copy the lib directory from your seam installation to my project folder to get the build working.