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:
- JSF 1.2 (Mojarra) with Facelets and RichFaces 3.2.2 SR1
- Hibernate (EntityManager, Annotations, and Validator)
- Seam 2.1.1.GA on JBoss 4.2.2.GA
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!
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).
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.
3. Remember Me
Registered users can request the site to remember their credentials for automatic login for future visits.
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.
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.
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.
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
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.
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;
}
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 SetgetUserRoles() { 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 SetgetRoleUsers() { 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.
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.
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.
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>
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.
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).
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; }
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
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.
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.
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 :-(
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
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.
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:
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.
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>
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.
Thanks, Tim. It's very useful as a good template for any web app.
ReplyDeletecool - thanks for the work, dude
ReplyDeleteGrate job.
ReplyDeleteIt is clear and useful and this highlights the power of this application.
Thnaks
I hate to see such a nice article with no comments. So, THANKS for the really nice article!
ReplyDeleteReally nice article!
ReplyDeleteAwesome, thanks so much for this article. I just started my first seam app last night and after creating the project with seam-gen copied this example over to help me get started.
ReplyDeleteLooking forward to your followup articles!
Thank you so much for this article.
ReplyDeleteHowever, after I explode and deployied the war file to my Jboss 5.0.0.CR2 JDK6, when I run the server i got an error: [AnnotationDeploymenthandler] could not load annotation class : javax.ws.rs.ext.provider
Please could you help me in this
RE: could not load annotation class : javax.ws.rs.ext.provider
ReplyDeleteI'm not sure as I only worked with JBoss 4.2.2 for this example. Have a look at this thread on the Seam forums:
http://seamframework.org/Community/Error
Thanks you For your reply
ReplyDeleteI have solved this issue as follows:
1- Used jboss 4.2.3.GA
2- Added jaxrs-api.jar to the deployed-jars.list file which exists in the project home directory
After I exploded and deployed the war file every thing worked as described above.
Thanks dude for this lovely article :)
One more question though :(
ReplyDeleteHow can I config an SMTP server for me to be able to receive the emails for this web application?
Thanks in advance
very, very nice, great job
ReplyDeleteThis should be incorporated in seam-gen
ReplyDeleteIt looks really nice.
Great article. However, you really need to learn the beauty of the StatusMessages component in Seam. It would make it a lot simpler to add FacesMessages to the response.
ReplyDelete...as for incorporating this into seam-gen, it's just too much for right now. We need to first have a modular system and then we can think about doing plugins such as a registration system.
Thanks for this great article, it helped me a lot.
ReplyDeletePlease, keep on posting such great articles.
RE: using StatusMessages ...
ReplyDeleteThanks for the tip ... StatusMessages really made adding localized FacesMessages much easier.
All is great, but it has been proven that Modal Panels are bad practice for building UI.
ReplyDeleteNevertheless, great article, thanks!
Proven? That's a strong statement ... Of course any UI component can be used incorrectly but I don't see how the solution here equates to "bad practice". Please elaborate vanyatka.
ReplyDeleteBrilliant article, I had been trying for some time to get the "remember me" functionality working but encountered a lot of the same problems you did and thought I was doing something wrong.
ReplyDeleteIt's great to see another good practical, functional walthrough and sample code provided and can serve as a great template for a new application. Hope you don't mind if I steal it :-)
really great article! thanks for posting the example and helping who is starting with Seam.
ReplyDeleteThis comment has been removed by the author.
ReplyDeletei had an exception at runtime :
ReplyDeleteorg.richfaces.skin.SkinNotFoundException: Skin with name glassX not found.
i added the glassX-3.3.1.GA.jar (found in jsf_rf_seam_user_reg\resources\WEB-INF\lib) to the \jboss-4.2.3.GA\server\default\lib directory .
thanks for the example
This is brilliant piece of text. Very good comments within the article as well. Any web application reqiures identity management. This one is perfect for any seam app. Thanks.
ReplyDeleteHi thelabdude,
ReplyDeleteThanks for the wonderful article. Could you add a link to your database file? I would like to run seam-gen myself and generate the project dir and then work through your steps.
Also, if possible, screenshot of options you chose for seam-gen and its explanation would be highly appreciated.
Thanks
sri
I'm trying to integrate some of this into a new project I am working on, and I have a question...
ReplyDeleteIn guestSupport.xhtml, I see lots of references to #{messages.xxxxxx} (e.g., login_username, login_password, login_email, firstName, lastName)
I don't see a class with an @Name("messages") though. Where is this messages class? Is it seam provided? If so, I can't find it. I throw a reference (see below) to it into my xhtml, and seam seemed to resolve it just fine, but I'm not sure how.
h:outputText value=#{messages.register}"
Thanks,
Tom
Glad to see you are integrating some of this ... hope it works out well for you ;-)
ReplyDeletemessages is a built-in Seam component (org.jboss.seam.international.messages)
Read more about it at:
http://docs.jboss.org/seam/latest/reference/en-US/html/components.html
Ahh, got it. All those are in messages dot properties.
ReplyDeleteThanks!
great site - thanks for the info Customized application development
ReplyDeleteIt's really a nice blog. I like it. It's really informative blog. Keep it up nice blogging.
ReplyDeleteAdd, add your website in www.directory.itsolusenz.com/
perfect, super
ReplyDeletehi! i like the designs. check out the source of the template.
ReplyDeleteThank you! i love it.
More templates easy to download
Great article!!! You were talking about dealing with the CRUD components of the Seam application framework in the future. You mean the "Home" component right? Are you already working on this article? I'm very curious about that one. Thanks again for this great article.
ReplyDeleteGreat article! It helped alot.
ReplyDeleteSilly question for you, how did you go about creating the modal panels? There is no preview while creating them in the editor and was wondering the way you went about creating them..
Thanks!!!
hi.. can anyone tell me how to get this application up and running on tomcat?
ReplyDeletecheers.
This is a very nicely written and useful page!
ReplyDeleteThanks for the effort.
I have been developing Liferay 5.2 Portlets with Seam 2.2.0 and JBoss 4.2 and this page will be a good handbook :)
Leo
Nice work!
ReplyDeleteOne of the best series of articles I've come across on the web. Well done!, and thanks so much for putting in all the effort and sharing your expertise and code with us. The Seam team, Dan Allen and yourself have made programming a lot more interesting :-)
ReplyDeleteThat was remarkable post. I like to read articles that are edifying for they enriched my mind with different knowledge that makes me a better person. These articles especially about recent events, technologies, news, tips and technical skills are the topics that I adore. Keep it up and more power to your website. I look forward for your next article.Thanks Jennifer Liferay Portal Development
ReplyDeleteHi,
ReplyDeleteI'm new with transactions and I have a doubt. In the doRegistration of RegisterAction class, all the doing the work is enclosed in a try-catch block without rethrowing it ou. If an exception occurs inside the block, will the transaction be rolled back? Would SEAM be able to notice it?
Hi mafa,
ReplyDeleteI think you are right in that we should explicitly mark the Tx to be rollback only in the catch block. The reason I didn't catch this before is that the only time I've seen an exception in this method is if Events.instance().raiseEvent("userRegistered"); fails, in which case Seam would have already marked the Tx as rollback only.
cool
ReplyDeleteThis is a really nice article for seam and The Seam creator Gavin agree too. http://relation.to/Bloggers/NiceArticleAboutSeam
ReplyDeleteThis is a very nice article. But I have a question: In the event that user wishes to change his/her password through the change password panel in the preference section, how do you verify current password?
ReplyDeleteHi Olalekan,
ReplyDeleteGood point! Requiring the user to enter their current password before changing it would definitely be a great feature to add to this impl.