Validation
VRaptor validation uses Bean Validation, spec present in Java EE 7, that allow us to validate our beans based on annotations. With this you can use embedded constraints or create your custom constraint.
public class Client {
// validates if name is not null and size between 10 and 50
@NotNull @Size(min=10, max=50) private String name;
// validates if birth date is past
@Past private Date birth;
}
And in your controller:
public class ClientController {
private final Validator validation;
/**
* @deprecated CDI eyes only
*/
protected ClientController() {
this(null);
}
@Inject
public ClientController(Validator validation) {
this.validation = validation;
}
public void form() {
}
public void store(@NotNull @Valid Client client) {
// if client is not valid, redirect to form page
validation.onErrorForwardTo(this).form();
}
}
Note: If your application uses Hibernate or JPA and you have your entities annotated with Bean Validation constraints, you need to call Session.flush()
or EntityManager.flush()
to validate your model before redirect or forward. The method flush
synchronizes state from your objects to database. So Bean Validation constraint will only validate when flush
is triggered.
Creating your custom constraints
With Bean validation you can create your own custom validations. If you want to validate if phone number is in (00) 0000-0000
pattern, you can do something like this:
@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@ReportAsSingleViolation
@Pattern(regexp = "\\((\\d{2})\\) (\\d{4})-(\\d{4})")
public @interface PhoneNumber {
String message() default "{br.com.caelum.vraptor.validations.phonenumber.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The message’s value with {}
will be fetched from ResourceBundle
.
Or you can put a fixed message, just like this:
String message() default "Invalid number";
You can create complex constraints that access database or uses any managed component. If you want, as example, check if the user already exists in the database, ou can write a code like this:
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { LoginAvailableValidator.class })
@Documented
public @interface LoginAvailable {
String message() default "{login_already_exists}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And LoginAvailableValidator
that checks the constraint:
public class LoginAvailableValidator implements ConstraintValidator<LoginAvailable, User> {
@Inject
private UserDao userDao;
public boolean isValid(User user, ConstraintValidatorContext context) {
return !userDao.containsUserWithLogin(user.getLogin());
}
}
Using custom messages
If you prefer, you can use custom messages with Bean Validation. For this you need to add the file ValidationMessages.properties
in your classpath. In a Mavem project, the location is /src/main/resources
.
Considering the previous example, the file might have the following content to customize the default messages:
client.name.empty = The client name may not be empty
client.name.size = The client name size must be between 10 and 50
client.birth.past = The birth must be in the future
And the class:
public class Client {
@NotNull(message="{client.name.empty}")
@Size(min=10, max=50, message="{client.name.size}")
private String name;
@Past(message="{client.birth.past}")
private Date birth;
}
The value {}
present in annotations indicates that the message comes from the ResourceBundle
.
See that was added parameters in order to let our most dynamic and reusable messages. The possibilities are currently:
- attribute values of the restriction mapped with the attribute names, like
{min}
and{max}
for@Size
- the current value to be validated with the name
${validatedValue}
- a ternary operation like:
${value > 1 ? 's' : ''}
- a bean mapped with the name
formatter
exposing a var-arg methodformat(String format, Object... args)
which behaves likejava.util.Formatter.format(String format, Object... args)
. Example:${formatter.format('%1$.2f', validatedValue)}
Validation using Validator class methods
If you don’t want to use Bean Validation, you can use other methods located at Validator
class. Below an example with add
method:
if (client.getName() == null) {
//an hard-coded message
validator.add(new SimpleMessage("name", "The name is mandatory"));
//i18n message
validator.add(new I18nMessage("name", "name.mandatory"));
}
You can use another methods like addIf
, that only adds the message if a precondition is true
, and ensure
that only adds the message if the precondition is false
:
validator.addIf(client.getName() == null, new SimpleMessage("name", "The name is mandatory"));
validator.ensure(client.getName() != null, new SimpleMessage("name", "The name is mandatory"));
Redirecting when a constraint error occurs
When a constraint error occurs, you can redirect or forward the user to another page. You can see below some of the possible actions:
validator.onErrorForwardTo(MusicController.class).list() ==> validator.onErrorUse(logic()).forwardTo(MusicController.class).list();
validator.onErrorRedirectTo(MusicController.class).list() ==> validator.onErrorUse(logic()).redirectTo(MusicController.class).list();
validator.onErrorUsePageOf(MusicController.class).list() ==> validator.onErrorUse(page()).of(MusicController.class).list();
validator.onErrorSendBadRequest() ==> validator.onErrorUse(status()).badRequest(errors);
If you want to redirect to the same controller, you can use:
validator.onErrorForwardTo(this).list() ==> validator.onErrorUse(logic()).forwardTo(this.getClass()).list();
validator.onErrorRedirectTo(this).list() ==> validator.onErrorUse(logic()).redirectTo(this.getClass()).list();
validator.onErrorUsePageOf(this).list() ==> validator.onErrorUse(page()).of(this.getClass()).list();
Print the errors in the view
When an error exists, VRaptor puts an attribute named errors
in the request scope, that contains a list with all constraint errors. This list contains a key-value object that represents:
-
category
: represents the full path of the attribute likeobject.attribute
-
message
: represents the error message like: “must be in the future”, “may not be empty”, and so on.
In the view you can do something like this:
<c:forEach var="error" items="${errors}">
${error.category} - ${error.message}<br />
</c:forEach>
You can search for an error for a specific category, that can be useful to display constraint the messages beside the field.
<input type="text" name="client.name" />
<span class="error">${errors.from('client.name')}</span>
It will print “may not be empty, size must be between 10 and 50”. You can also use the join method if you want to use another separator, just like this:
<span class="error">${errors.from('client.name').join(' - ')}</span>
That will print “may not be empty - size must be between 10 and 50”.
Adding more messages
When you add a message, the framework will add as an error. But its possible to add the message as success, info or warning. In this case you can simple use available attributes from Severity
enum as you can see below:
validator.add(new SimpleMessage("client.name", "name field have accents", Severity.WARN));
And in the view you can use vmessages
attribute:
<input type="text" name="client.name" />
<span class="error">${vmessages.warnings.from('client.name')}</span>
This code will print “name field have accents” beside the input text.
Programmatic validation
If you want, you can use the vraptor-simple-validator plugin to do your validations in a fluent way. It provide us a diversity of defaults validations that you may want to use.