Forms and validation in Play Framework

When user can't interact with the application, he loses the interest for it. It's why forms and other input methods are a useful to satisfy the final user demand. Play Framework also supports operations on forms, as generation from POJO or dynamic validation with annotations.

In the first part of the article, we'll see how to implement forms based on POJO models in Play Framework. The second part will be destined to customized parts, as validator or supplementary fields. At the end we'll see how the form behaves against values which aren't associated with the model.

Forms and validation in Play Framework

To see how does the form work, we need to create a new table in the database. As in the case of sessions in Play Framework, we'll write a evolution script and call it 2.sql:

# --- !Ups
CREATE TABLE IF NOT EXISTS users (
  id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  login VARCHAR(10) NOT NULL,
  passa VARCHAR(255) NOT NULL,
  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(id)
) ENGINE=InnoDB;

# --- !Downs
DROP TABLE users;

As you can see, it creates users table with some basic fields. Now, we must create JPA entity:

@Entity
@Table(name="users")
public class User { 
    private int id; 
    @Required(message = "Login is mandatory field")
    @MinLength(value = 5, message = "Login must be at least 5-characters text")
    @MaxLength(value = 10, message = "Login can be 10-characters max length")
    @Pattern(value = "[a-z0-9\\-]+", message = "Only alphanumerical lower case characters and - are allowed in this field.")
    private String login;
    @Required(message = "Password is mandatory field")
    private String password;
    private Date birthday;
    private Date createdTime;
 
    @Id
    @GeneratedValue(strategy = IDENTITY)
	@Column(name="id")
	public int getId() {
		return this.id;
	}
	@Column(name="login")
	public String getLogin() {
		return this.login;
	}
	@Column(name="passa")
	public String getPassword() {
		return this.password;
	}
	@Temporal(TIMESTAMP)
	@Column(name="created")
	public Date getCreatedTime() {
		if (this.createdTime == null) {
			setCreatedTime(new Date());
		}
		return this.createdTime;
	}
	@Temporal(DATE)
	@Column(name="birthday")
	public Date getBirthday() {
		return this.birthday;
	}
	@Transient
	public String getSalt() {
		return String.valueOf(this.login.charAt(4))+this.birthday.getTime()+String.valueOf(this.login.charAt(0));
	}
	
	public void setId(int id) {
		this.id = id;
	}
	public void setLogin(String login) {
		this.login = login;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public void setCreatedTime(Date createdTime) {
		this.createdTime = createdTime;
	}
	public void setBirthday(Date birthday) {
		this.birthday = birthday;
	}

	@Override
	public String toString() {
		return "User {id: "+this.id+", login: "+this.login+"}";
	}
	
}

Class structure is simple. It's composed only by setters and getters. Play-based things are only the annotations on private fields. Yes, there are some validation annotations:
- @Required to annotate required field
- @Maxlength and @Minlength to handle, respectively, max and min length of the field value
- @Pattern to decide which characters are allowed to the field

The forms in Play are created with play.data.Form<T> object. Take a look on our UserController to see it:

public class UserController extends Controller {

   /**
    * This Form object is common for all controllers. It's a empty version of register form. We create the version of form
    * handled by submit method (registerSubmit) with userForm.bindFromRequest().
    */
    private static Form<User> userForm = Form.form(User.class);
	
    @Transactional(readOnly=true)
    public static Result register() {
        return ok(register.render(userForm));
    }

    @Transactional()
    public static Result registerSubmit() {
    	Form<User> submittedForm = userForm.bindFromRequest();
    	if (!submittedForm.hasErrors()) {
        	User user = submittedForm.get();
        	UserService userService = (UserService) ServicesInstances.USER_SERVICE.getService();
        	boolean added = userService.addNewUser(user);
        	if (added) {
        		// TODO : redirect to success page
        	} else {
        		// TODO : pass error message to the template
        	}
        	Logger.debug("Found user :"+user);
    	}
    	return ok(register.render(submittedForm));
    }
}

As you can see, the controller stores a private static userForm object. This object is shared by all users calling register page (view rendered by register() method). This register form is the same for all users. And it's a safe because it changes only after the user submits it. This moment is handled in registerSubmit() method where we create a new form depending on the executed request (userForm.bindFromRequest()). Play contains several different bind() methods. Some of them supports JSON or request values, the others Map<String, String[]> instances. They all point internally to public Form<T> bind(Map<String,String> data, String... allowedFields) method. As the name of this method suggest, it's responsible for handling submitted data. It means that inside this method we can find a fragments about:

If you're familiar with Spring Framework, the code snippet saw in the previous list should hit your eyes. Yes, it's the same as in the case of validation in Spring. Even more, Play uses Spring objects to work with errors (BindingResult) or handle request data (DataBinder).

Customize forms in Play Framework

Play can handle two types of error:

This validate() method was the first way of customize Play Framework. Another way consist on create our own validator. As you can see in the entity class, they're a birthday field. We want that it remains valid. To achieve it, we can create a validator and accept only dates in the past. To do that, we must start by writing annotation:

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = BeforeDateValidator.class)
@play.data.Form.Display(name="BeforeDate")
public @interface BeforeDate {
    String message() default "The date can't be a future date";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String dateToCompare() default BeforeDateValidator.NOW;
}

Only dateToCompare attribute is particular. As its name indicates, it stores date used to comparison with the date introduced by the user. In this case, this date corresponds to today. And it's normal in the case of birthday date because we wont the date being before today (so in the past). The BeforeDateValidator is based on this dependency to decide if one date is valid or not:

public class BeforeDateValidator extends Constraints.Validator<Date> implements ConstraintValidator<BeforeDate, Date>  {

	public static final String NOW = "now";
	private Date dateToCompare;
	
	@Override
	public void initialize(BeforeDate annot) {
		if (annot.dateToCompare().equals(NOW)) {
			dateToCompare = new Date();
		}
	}

	@Override
	public Tuple<String, Object[]> getErrorMessageKey() { 
		return new Tuple("beforeDate", new Object() {});
	}

	@Override
	public boolean isValid(Date toValid) {
		Logger.debug("Validating "+toValid);
		return toValid != null && toValid.before(dateToCompare);
	}

}

Now, we should annotate a field validated with @BeforeDate constraint. However, it won't be enough. Play doesn't know the way to handle Date fields. To handle them properly and, allow the validation, we need to create an formatter. After this formatted will be applied to fields annotated with @Birthday annotation:

@Target({FIELD,METHOD})
@Retention(RUNTIME)
public @interface Birthday {

	String format() default "dd/MM/yyyy";
	
}

The formatter is an instance of AnnotationFormatter (it depends on one annotation) and it bases the conversion (Date <=> String) on format attribute of @Birthday annotation:

public class BirthdayFormatter extends AnnotationFormatter<Birthday, Date> {
	/**
	 * Converts form input to expected Java's object.
	 * 
	 * @return Converted Date or null if an exception occurred.
	 */
	@Override
	public Date parse(Birthday annot, String input, Locale locale) throws ParseException {
		try {
			return FromStringConverter.toDate(input, annot.format());
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Converts Java object to form input.
	 * 
	 * @return String representation of Date object or empty String if an exception occurred.
	 */
	@Override
	public String print(Birthday annot, Date dateObj, Locale locale) {
		try {
			return FromDateConverter.toString(dateObj, annot.format());
		} catch (Exception e) {
			return ConstantsContainer.EMPTY_STRING;
		}
	}

}

The last thing to do is to activate the BirthdayFormatter. We should to register it in one method called every time when the application starts and restarts. We opt for a static bloc inside configuration class (StoreGlobalSettings):

static {
	Logger.debug("------------- Setting formatters ---------------");
	Formatters.register(Date.class, new BirthdayFormatter());
};

Now, if you look carefully in the logs, you should see following entry at the application setup:

[debug] application - ------------- Setting formatters ---------------

And this one when you submit register form:

[debug] application - Validating Wed Nov 20 00:00:00 CET 2013
[debug] application - Validating user on validate() method

Mass assignment in Play Framework

Forms in Play Framework are nice and simple (as in Spring by the way), but there're some drawback too. The main is the risk of mass assignment. A mass assignment is an attack consisting on submitting fields belonging to form Java object which shouldn't be accepted as input values. To understand this better, imagine our User class with a supplementary field: isMajor. The value of this field is specified after register by administrator. If this value is true, user will receive some gifts every time when he shows his activity in the site. Let's make some tests by adding this field in the database and User class:

private int major;

@Column(name="major")
public int getMajor() {
	return this.major;
}

public void setMajor(int major) {
	this.major = major;
}

@Transient
public boolean isMajor() {
	return this.major == 1;
}

Our form remains at the same state. Let's see what happens if we create manually <input type="hidden" name="major" value="1" /> and submit it into registerSubmit() method (we added simple debug before UserService call Logger.debug("Data is :"+submittedForm.data()) and Logger.debug("User from form is: "+user)) :

[debug] application - Data is :{register=register, birthday=20/11/2013, login=bartosz4, password=bartosz4, major=1}
[debug] application - User from form is: User {id: 0, login: bartosz2, is major: true}

As you can see, user is major before the administrator decision. One of protections against this kind of dangers is the precision of accepted form fields in bindFromRequest method, as following:

@Transactional()
public static Result registerSubmit() {
	Form<User> submittedForm = userForm.bindFromRequest("login", "password", "birthday");

Thanks to it, only login, password and birthday fields will be interpreted by the form. So even if you try to bypass it by adding major field, it won't be taken in consideration by the bind() method saw in the first part of this article:

[debug] application - Data is :{register=register, birthday=20/11/2013, login=bartosz4, password=bartosz4, major=1}
[debug] application - User from form is:User {id: 0, login: bartosz4, is major: false}

This article shows how form works in Play Framework. The first part presented the way of integrating JPA entities (or another Java objects) to Play's form mechanism. A mechanism which is composed by Spring validation components. After that we discovered how to customize forms in Play with annotations, formatters and global validation methods. The last part explained a little bit about mass assignment danger.

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!