Home > JSF, Open Lane, Spring, Spring Web Flow > Open Lane Sign Up

Open Lane Sign Up

Time for Open Lane to get more real with a user sign up feature. The application already has some user management along with authentication and authorization. Now it needs the ability for someone to sign up and create a user account. Here’s what needs to change.

Add a table and service for all eligible users (swimmers). It contains an unique identifier for each swimmer along with important name and demographic information. The data for this table is loaded from another system and should exist prior to a user attempting to sign up. The sign up process involves gathering some additional information and associating (linking) the user account with their pre-existing swimmer data.

I created entity and service classes to deal with this data. These classes cover the basics.

Swimmer.java

package org.bwgz.swim.openlane.model;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "Swimmers")
public class Swimmer implements Serializable {
	private static final long serialVersionUID = 1816256078842365678L;

	private String id;
	private String first;
	private String last;
	private String middle;
	private String gender;
	private Date birthdate;

	public Swimmer() {
	}

	public Swimmer(String id) {
		this.id = id;
	}

	@Id
	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getFirst() {
		return first;
	}

	public void setFirst(String first) {
		this.first = first;
	}

	public String getLast() {
		return last;
	}

	public void setLast(String last) {
		this.last = last;
	}

	public String getMiddle() {
		return middle;
	}

	public void setMiddle(String middle) {
		this.middle = middle;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

	public Date getBirthdate() {
		return birthdate;
	}

	public void setBirthdate(Date birthdate) {
		this.birthdate = birthdate;
	}

	public static long getSerialversionuid() {
		return serialVersionUID;
	}

	@Override
        public String toString() {
              return String.format("%s@%x; Id: %s; First: %s; Middle: %s; Last: %s; Gender: %s; Birthdate: %s;",
              this.getClass().getName(), this.hashCode(),
              getId(), getFirst(), getMiddle(), getLast(), getGender(), getBirthdate());
}

}

SwimmerService.java

package org.bwgz.swim.openlane.service;

import java.util.List;

import org.bwgz.swim.openlane.model.Swimmer;

public interface SwimmerService {
public List<Swimmer> findSwimmers(String id);
public Swimmer findSwimmer(String id);
}

SwimmerServiceImpl.java

package org.bwgz.swim.openlane.service;

import java.io.Serializable;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.bwgz.swim.openlane.model.Swimmer;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Service("swimmerService")
@Repository
public class SwimmerServiceImpl implements SwimmerService, Serializable {
	private static final long serialVersionUID = -7264545602862288436L;

	private EntityManager em;

	@PersistenceContext
	public void setEntityManager(EntityManager em) {
		this.em = em;
	}

	private Query findSwimmerQuery(String id) {
		return em.createQuery("select u from Swimmer u where u.id = :id")
				.setParameter("id", id);
	}

	@SuppressWarnings("unchecked")
	public List<Swimmer> findSwimmers(String id) {
		List<Swimmer> list = null;

		if (id != null) {
			list = (List<Swimmer>) findSwimmerQuery(id).getResultList();
		}

		return list;
	}

	public Swimmer findSwimmer(String id) {
		Swimmer swimmer = null;

		if (id != null) {
			try {
				swimmer = (Swimmer) findSwimmerQuery(id).getSingleResult();
			} catch (Exception e) {
			}
		}

		return swimmer;
	}

}

Not just anyone can sign up for a user account. Only someone who’s eligible to participate in a swim meet can apply for an open lane. The process has to account for this when a prospective user signs up. I created an JSF Validator to check if the id given during sign-up matches an existing swimmer. If it does not then the Validator will throw an exception and the JSF form will catch it.

USwimmerValidator.java

package org.bwgz.swim.openlane.faces.validator;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

import org.bwgz.swim.openlane.model.Swimmer;
import org.bwgz.swim.openlane.service.SwimmerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class USwimmerValidator implements Validator {

	@Autowired
	private SwimmerService service;

	public void setService(SwimmerService service) {
		this.service = service;
	}

	public void validate(FacesContext context, UIComponent component,
			Object value) throws ValidatorException {
		System.out.printf("USwimmerValidator.validate(%s, %s, %s)\n", context,
				component, value);
		System.out.printf("\t component.getId: %s\n", component.getId());
		System.out.printf("\t component.getClientId: %s\n",
				component.getClientId(context));
		System.out.printf("\t component.getContainerClientId: %s\n",
				component.getContainerClientId(context));
		System.out.printf("\t service: %s\n", service);

		Swimmer membership = service.findSwimmer((String) value);
		if (membership == null) {
			FacesMessage message = new FacesMessage();
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			message.setSummary("Swimmer not found.");
			message.setDetail("Swimmer not found.");
			context.addMessage(component.getClientId(context), message);
			throw new ValidatorException(message);
		}
	}
}

I decided to create a sub-flow to handle the sign up process. I also needed a backing bean for the sign up form. The heart of the flow involves capturing the username, email, password, and swimmer id in a JSF form, validating the input, and then creating the user account.

On start up the sub-flow instantiates a backing bean (signupBean) which will be used throughout the sub-flow and then discarded. A sign up page (signup.xhtml) is called by the view state and when the form on that page is submitted the flow will execute the action state adduser. adduser calls the User service to create a user record in the the database. If that succeeds the sub flow exits. If not, it takes the user back to the sign up form for another attempt. At anytime during the sub flow the user can exit by selecting a home link.

signup-flow.xml

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
		http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

	<on-start>
            <evaluate expression="signupBean" result="flowScope.signup" />
	</on-start>

	<view-state id="signup">
		<transition on="submit" to="adduser"/>
		<transition on="home" to="home"/>
	</view-state>

	<action-state id="adduser">
		<evaluate expression="userService.addUser(signup, swimmerService.findSwimmer(signup.usasId))" />
                <transition on="yes" to="success" />
                <transition on="no" to="signup" />
	</action-state>

	<end-state id="home"/>
	<end-state id="success"/>

</flow>

All the of input fields on the sign up page are validated. That validation is defined using annotations in Setup.java. There’s also the JSF Validator mentioned above. The user can’t get past the sign-up page unless they enter legitimate answers.

Setup.java

package org.bwgz.swim.openlane.model;

import java.io.Serializable;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@ManagedBean
@RequestScoped
public class SignUp implements Serializable {
	private static final long serialVersionUID = 4957886416619036377L;

	@Size(min = 5, max=20, message = "Please enter a valid username (5-20 characters)")
	private String username;

	@Size(min = 5, max=20, message = "Please enter a valid password (5-20 characters)")
	private String password;

	@Size(min = 1, message = "Please enter the Email")
	@Pattern(regexp = "[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+", message = "Email format is invalid.")
	private String email;

	@Size(min = 14, max=14, message = "Please enter a valid USA Swimming ID (14 characters)")
	@Pattern(regexp = "[a-zA-Z0-9]+", message = "USA Swimming ID format is invalid.")
	private String usasId;

	public SignUp() {
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getUsasId() {
		return usasId;
	}

	public void setUsasId(String usasId) {
		this.usasId = usasId;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

    @Override
    public String toString() {
    	return String.format("%s@%x; Username: %s; Password: %s; Email: %s; UsasId: %s;",
    			this.getClass().getName(), this.hashCode(),
    			getUsername(), getPassword(), getEmail(), getUsasId());
    }
}

Here’s the the JSF form code from signup.xhtml.

<h:form id="signup">
	<h:outputLabel>User Name: </h:outputLabel>
	<h:inputText id="username" value="${signup.username}"/>
	<br/>
	<h:outputLabel>Password: </h:outputLabel>
	<h:inputText id="password" value="${signup.password}"/>
	<br/>
	<h:outputLabel>Your Email: </h:outputLabel>
	<h:inputText id="email" value="${signup.email}"/>
	<br/>
	<h:outputLabel>Your USA Swimming Id: </h:outputLabel>
	<h:inputText id="usasId" value="${signup.usasId}">
	    <f:validator binding="${USwimmerValidator}"/>
	</h:inputText>

	<br/>
	<h:commandButton id="submit" action="submit" value="Sign Up" update="@form" />
</h:form>

Note the validator tag on the Id inputText tag. I took me quite some time to get this to work correctly.

First, I created the Validator using @FacesValidator and  then added  @Autowired to the SwimmerService variable is needed during validation. It looked something like this.

@FacesValidator("swimmerValidator")
public class USwimmerValidator implements Validator {

    @Autowired
    private SwimmerService service;

This only half worked. The JSF form was able to execute the validator but SwimmerService was not set. After scouring the Internet I learned that the validator was know to JSF but not Spring. I had to drop @FacesValidator and go with @Component inside. This meant in the JSF form I needed to use binding instead of validatorId.

Second, my code original used a different Validator class name called USASMemberValidator. When I refactored it to SwimmerValidator the auto-wiring stopped working. I don’t know why. When I renamed it to USwimmerValidator everything worked fine. Something like this shouldn’t happen. This isn’t the first time in my career that I’ve seen something as flaky as this but it surprised me that I’d see it here.

While investigating all this I came across javax.inject (@Inject). I’m going to looking into this as an alternative.

Finally, I re-factored User by adding a Swimmer variable and annotating it with a one to one relationship. Here’s a snippet from User.java.

	@OneToOne(fetch=FetchType.EAGER)
	public Swimmer getSwimmer() {
		return swimmer;
	}

This is a unidirectional relationship and sufficient for now. I’ll probably re-factor it to a bidirectional relationship to provide some referential capabilities. I also need to ensure that when when a user record is deleted the associated swimmer record is not.

Next step is to get down to the business of an open lane application submission. More on that in my next Open Lane post.

As always, the code is available here on GitHub.

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: