§Handling form submission
Before you start with Play forms, read the documentation on the Play enhancer. The Play enhancer generates accessors for fields in Java classes for you, so that you don’t have to generate them yourself. You may decide to use this as a convenience. All the examples below show manually writing accessors for your classes.
§Defining a form
The play.data
package contains several helpers to handle HTTP form data submission and validation. The easiest way to handle a form submission is to define a play.data.Form
that wraps an existing class:
public class User {
protected String email;
protected String password;
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
To wrap a class you have to inject a play.data.FormFactory
into your Controller which then allows you to create the form:
Form<User> userForm = formFactory.form(User.class);
Note: The underlying binding is done using Spring data binder.
This form can generate a User
result value from HashMap<String,String>
data:
Map<String,String> anyData = new HashMap<>();
anyData.put("email", "[email protected]");
anyData.put("password", "secret");
User user = userForm.bind(anyData).get();
If you have a request available in the scope, you can bind directly from the request content:
User user = userForm.bindFromRequest().get();
§Defining constraints
You can define additional constraints that will be checked during the binding phase using JSR-303
(Bean Validation 1.0) annotations:
public class User {
@Required
protected String email;
protected String password;
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
Tip: The
play.data.validation.Constraints
class contains several built-in validation annotations.
In the Advanced validation section further below you will learn how to handle concerns like cross field validation, partial form validation or how to make use of injected components (e.g. to access a database) during validation.
§Handling binding failure
Of course if you can define constraints, then you need to be able to handle the binding errors.
if (userForm.hasErrors()) {
return badRequest(views.html.form.render(userForm));
} else {
User user = userForm.get();
return ok("Got user " + user);
}
Typically, as shown above, the form simply gets passed to a template. Global errors can be rendered in the following way:
@if(form.hasGlobalErrors) {
<p class="error">
@for(error <- form.globalErrors) {
<p>@error.format(messages())</p>
}
</p>
}
Errors for a particular field can be rendered in the following manner with error.format
:
@for(error <- form("email").errors) {
<p>@error.format(messages())</p>
}
Note that error.format
takes messages()
as an argument – this is an play.18n.Messages
instance defined in JavaI18n.
§Filling a form with initial default values
Sometimes you’ll want to fill a form with existing values, typically for editing:
userForm = userForm.fill(new User("[email protected]", "secret"));
Tip:
Form
objects are immutable - calls to methods likebind()
andfill()
will return a new object filled with the new data.
§Handling a form that is not related to a Model
You can use a DynamicForm
if you need to retrieve data from an html form that is not related to a Model
:
public Result hello() {
DynamicForm requestData = formFactory.form().bindFromRequest();
String firstname = requestData.get("firstname");
String lastname = requestData.get("lastname");
return ok("Hello " + firstname + " " + lastname);
}
§Register a custom DataBinder
In case you want to define a mapping from a custom object to a form field string and vice versa you need to register a new Formatter for this object.
You can achieve this by registering a provider for Formatters
which will do the proper initialization.
For an object like JavaTime’s LocalTime
it could look like this:
import java.text.ParseException;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import java.time.LocalTime;
import play.data.format.Formatters;
import play.data.format.Formatters.SimpleFormatter;
import play.i18n.MessagesApi;
@Singleton
public class FormattersProvider implements Provider<Formatters> {
private final MessagesApi messagesApi;
@Inject
public FormattersProvider(MessagesApi messagesApi) {
this.messagesApi = messagesApi;
}
@Override
public Formatters get() {
Formatters formatters = new Formatters(messagesApi);
formatters.register(LocalTime.class, new SimpleFormatter<LocalTime>() {
private Pattern timePattern = Pattern.compile(
"([012]?\\d)(?:[\\s:\\._\\-]+([0-5]\\d))?"
);
@Override
public LocalTime parse(String input, Locale l) throws ParseException {
Matcher m = timePattern.matcher(input);
if (!m.find()) throw new ParseException("No valid Input", 0);
int hour = Integer.valueOf(m.group(1));
int min = m.group(2) == null ? 0 : Integer.valueOf(m.group(2));
return LocalTime.of(hour, min);
}
@Override
public String print(LocalTime localTime, Locale l) {
return localTime.format(DateTimeFormatter.ofPattern("HH:mm"));
}
});
return formatters;
}
}
After defining the provider you have to bind it:
import com.google.inject.AbstractModule;
import play.data.format.Formatters;
public class FormattersModule extends AbstractModule {
@Override
protected void configure() {
bind(Formatters.class).toProvider(FormattersProvider.class);
}
}
Finally you have to disable Play’s default FormattersModule
and instead enable your module in application.conf
:
play.modules.enabled += "com.example.FormattersModule"
play.modules.disabled += "play.data.format.FormattersModule"
When the binding fails an array of errors keys is created, the first one defined in the messages file will be used. This array will generally contain:
["error.invalid.<fieldName>", "error.invalid.<type>", "error.invalid"]
The errors keys are created by Spring DefaultMessageCodesResolver, the root “typeMismatch” is replaced by “error.invalid”.
§Advanced validation
Play’s built-in validation module is using Hibernate Validator under the hood. This means we can take advantage of features defined in the JSR-303
(Bean Validation 1.0) and JSR-349
(Bean Validation 1.1). The Hibernate Validator documentation can be found here.
§Cross field validation
To validate the state of an entire object we can make use of class-level constraints.
To free you from the burden of implementing your own class-level constraint(s), Play out-of-the-box already provides a generic implementation of such constraint which should cover at least the most common use cases.
Now let’s see how this works: To define an ad-hoc validation, all you need to do is annotate your form class with Play’s provided class-level constraint (@Validate
) and implement the corresponding interface (in this case Validatable<String>
) - which forces you to override a validate
method:
import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
@Validate
public class User implements Validatable<String> {
@Constraints.Required
protected String email;
protected String password;
@Override
public String validate() {
if (authenticate(email, password) == null) {
// You could also return a key defined in conf/messages
return "Invalid email or password";
}
return null;
}
// getters and setters
}
The message returned in the above example will become a global error. Errors are defined as play.data.validation.ValidationError
.
Also be aware that in this example the validate
method and the @Constraints.Required
constraint will be called simultaneously - so the validate
method will be called no matter if @Constraints.Required
was successful or not (and vice versa). You will learn how to introduce an order later on.
As you can see the Validatable<T>
interface takes a type parameter which determines the return type of the validate()
method.
So depending if you want to be able to add a single global error, one error (which could be global as well) or multiple (maybe global) errors to a form via validate()
, you have to use either a String
, a ValidationError
or a List<ValidationError>
as type argument. Any other return types of the validate method will be ignored by Play.
If validation passes inside a validate()
method you must return null
or an empty List
. Returning any other non-null
value (including empty string) is treated as failed validation.
Returning a ValidationError
object may be useful when you have additional validations for a specific field:
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
@Validate
public static class LoginForm implements Validatable<ValidationError> {
// fields, getters, setters, etc.
@Override
public ValidationError validate() {
if (authenticate(email, password) == null) {
// Error will be displayed for the email field:
return new ValidationError("email", "Invalid credentials");
}
return null;
}
}
You can add multiple validation errors by returning List<ValidationError>
. This can be used to add validation errors for a specific field, global errors or even a mix of these options:
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import java.util.List;
@Validate
public static class SignUpForm implements Validatable<List<ValidationError>> {
// fields, getters, setters, etc.
@Override
public List<ValidationError> validate() {
final List<ValidationError> errors = new ArrayList<>();
if (authenticate(email, password) == null) {
// Add an error which will be displayed for the email field:
errors.add(new ValidationError("email", "Access denied"));
// Also add a global error:
errors.add(new ValidationError("", "Form could not be submitted"));
}
return errors;
}
}
As you can see, when using an empty string as the key of a ValidationError
it becomes a global error.
One more thing: Instead of writing out error messages you can use message keys defined in conf/messages
and pass arguments to them. When displaying the validation errors in a template the message keys and it’s arguments will be automatically resolved by Play:
// Global error without internationalization:
new ValidationError("", "Errors occured. Please check your input!");
// Global error; "validationFailed" should be defined in `conf/messages` - taking two arguments:
new ValidationError("", "validationFailed", Arrays.asList(arg1, arg2));
// Error for the email field; "emailUsedAlready" should be defined in `conf/messages` - taking the email as argument:
new ValidationError("email", "emailUsedAlready", Arrays.asList(email));
§Partial form validation via groups
When a user submits a form there can be use cases where you don’t want to validate all constraints at once but just some of them. For example think about a UI wizard where in each step only a specified subset of constraints should get validated.
Or think about the sign-up and the login process of a web application. Usually for both processes you want the user to enter an email address and a password. So these processes would require almost the same forms, except for the sign-up process the user also has to enter a password confirmation. To make things more interesting let’s assume a user can also change his user data on a settings page when he is logged in already - which would need a third form.
Using three different forms for such a case isn’t really a good idea because you would use the same constraint annotations for most of the form fields anyway. What if you have defined a max-length constraint of 255 for a name
field and then want to change it to a limit of just 100? You would have to change this for each form. As you can imagine this would be error prone in case you forget to update one of the forms.
Luckily we can simply group constraints:
import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import javax.validation.groups.Default;
@Validate(groups = {SignUpCheck.class})
public class PartialUserForm implements Validatable<ValidationError> {
@Constraints.Required(groups = {Default.class, SignUpCheck.class, LoginCheck.class})
@Constraints.Email(groups = {Default.class, SignUpCheck.class})
private String email;
@Constraints.Required
private String firstName;
@Constraints.Required
private String lastName;
@Constraints.Required(groups = {SignUpCheck.class, LoginCheck.class})
private String password;
@Constraints.Required(groups = {SignUpCheck.class})
private String repeatPassword;
@Override
public ValidationError validate() {
if (!checkPasswords(password, repeatPassword)) {
return new ValidationError("repeatPassword", "Passwords do not match");
}
return null;
}
// getters and setters
}
The SignUpCheck
and LoginCheck
group are defined as two interfaces:
public interface SignUpCheck { }
public interface LoginCheck { }
For the sign-up process we simply pass the SignUpCheck
group to the form(...)
method:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, SignUpCheck.class).bindFromRequest();
In this case the email address is required and has to be a valid email address, both the password and the password confirmation are required and the two passwords have to be equal (because of the @Validate
annotation which calls the validate
method). But we don’t care about the first name and last name - they can be empty or we could even exclude these input fields in the sign up page.
For the login process we just pass the LoginCheck
group instead:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, LoginCheck.class).bindFromRequest();
Now we only require the email address and the password to be entered - nothing more. We don’t even care about if the email is valid. You probably wouldn’t display any of the other form fields to the user because we don’t validate them anyway.
Imagine we also have a page where the user can change the user data (but not the password):
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, Default.class).bindFromRequest();
Which is exactly the same as:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class).bindFromRequest();
In this case following constraints will be validated: The email address is required and has to be valid plus the first name and last name are required as well - that is because if a constraint annotation doesn’t explicitly define a group
then the Default
group is used.
Be aware we don’t check any of the password constraints: Because they explicitly define a group
attribute but don’t include the Default
group they won’t be taken into account here.
As you can see in the last example, when only passing the group javax.validation.groups.Default
you can omit it - because it’s the default anyway.
But as soon you pass any other group(s) you would also have to pass the Default
group explicitly if you want any of it’s fields taken into account during the validation process.
Tip: You can pass as many groups as you like to the
form(...)
method (not just one). Just to be clear: These groups will then be validated all at once - not one after the other.
For advanced usage a group of constraints can include another group. You can do that using group inheritance.
§Defining the order of constraint groups
You can validate groups in sequences. This means groups will be validated one after another - but the next group will only be validated if the previous group was validated successfully before. (However right now it’s not possible to determine the order of how constraints will be validated within a group itself - this is part of JSR-380
- Bean Validation 2.0 - which is still in draft)
Based on the example above let’s define a group sequence:
import javax.validation.GroupSequence;
import javax.validation.groups.Default;
@GroupSequence({ Default.class, SignUpCheck.class, LoginCheck.class })
public interface OrderedChecks { }
Now we can use it:
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, OrderedChecks.class).bindFromRequest();
Using this group sequence will first validate all fields belonging to the Default
group (which again also includes fields that haven’t defined a group at all). Only when all the fields belonging to the Default
group pass validation successfully, the fields belonging to the SignUpCheck
will be validated and so on.
Using a group sequence is especially a good practice when you have a validate
method which queries a database or performs any other blocking action: It’s not really useful to execute the method at all if the validation fails at it’s basic level (email is not valid, number is a string, etc). In such a case you probably want the validate
be called only after checking all other annotation-based constraints before and only if they pass. A user, for example, who signs up should enter a valid email address and only if it is valid a database lookup for the email address should be done afterwards.
§Custom class-level constraints with DI support
Sometimes you need more sophisticated validation processes. E.g. when a user signs up you want to check if his email address already exists in the database and if so validation should fail.
Because constraints support Dependency Injection we can easily create our own custom (class-level) constraint which gets a Database
object injected - which we can use later in the validation process. (Actually you could also inject other components like MessagesApi
, JPAApi
, etc.)
You only need to create one class-level constraint for each cross concern.
For example: The constraint we will create in this section is reusable and can be used for all validation processes where you need to access the database. The reason why Play doesn’t provide any generic class-level constraints with dependency injected components is because Play doesn’t know which components you might have enabled in your project.
First let’s set up the interface with the validate
method we will implement in our form later. You can see the method gets passed a Database
object (Checkout the database docs):
import play.db.Database;
public interface ValidatableWithDB<T> {
public T validate(final Database db);
}
We also need the class-level annotation we put on our form class:
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidateWithDBValidator.class)
public @interface ValidateWithDB {
String message() default "error.invalid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Finally this is how our constraint implementation looks like:
import java.util.List;
import javax.inject.Inject;
import javax.validation.ConstraintValidatorContext;
import play.data.validation.Constraints.PlayConstraintValidator;
import play.db.Database;
public class ValidateWithDBValidator implements PlayConstraintValidator<ValidateWithDB, ValidatableWithDB<?>> {
private Database db;
@Inject
public ValidateWithDBValidator(final Database db) {
this.db = db;
}
@Override
public void initialize(final ValidateWithDB constraintAnnotation) {
}
@Override
public boolean isValid(final ValidatableWithDB<?> value, final ConstraintValidatorContext constraintValidatorContext) {
return reportValidationStatus(value.validate(this.db), constraintValidatorContext);
}
}
As you can see we inject the Database
object into the constraint’s constructor and use it later when calling validate
.
When writing your own class-level constraints you can pass following objects to the reportValidationStatus
method: A ValidationError
, a List<ValidationError>
or a String
(handled as global error). Any other objects will be ignored by Play.
Finally we can use our custom class-level constraint to validate a form:
import play.data.validation.Constraints;
import play.data.validation.ValidationError;
import play.db.Database;
@ValidateWithDB
public class DBAccessForm implements ValidatableWithDB<ValidationError> {
@Constraints.Required
@Constraints.Email
private String email;
@Constraints.Required
private String firstName;
@Constraints.Required
private String lastName;
@Constraints.Required
private String password;
@Constraints.Required
private String repeatPassword;
@Override
public ValidationError validate(final Database db) {
// Access the database to check if the email already exists
if (User.byEmail(email, db) != null) {
return new ValidationError("email", "This e-mail is already registered.");
}
return null;
}
// getters and setters
}
Tip: You might have recognised that you could even implement multiple interfaces and therefore add multiple class-level constraint annotations on your form class. Via validation groups you could then just call the desired validate method(s) (or even multiple at once during one validation process).