{#========================================== Spincast Properties File Config ==========================================#} {% extends "../../layout.html" %} {% block sectionClasses %}plugins plugins-spincast-validation{% endblock %} {% block meta_title %}Plugins - Spincast Validation{% endblock %} {% block meta_description %}Validation for your beans/models.{% endblock %} {% block scripts %} {% endblock %} {% block body %}
This plugin provides classes and patterns to help validate your beans/models.
The validation pattern this plugin promotes is not one using annotations (like Hibernate's validator, for example). It targets developers who don't like to pollute their models with too many annotations.
By using this plugin and the mix-ins
provided by the Spincast Jackson Json plugin and
the Spincast Jackson XML plugin, you can have models
which are not bloated with annotations, but that can still be (de)serialized (from/to Json
and XML) and can still be validated properly.
Add this artifact to your project:
<dependency>
<groupId>org.spincast</groupId>
<artifactId>spincast-plugins-validation</artifactId>
<version>{{spincastCurrrentVersion}}</version>
</dependency>
Then, install the plugin's Guice module, by passing it to the Guice.createInjector(...) method:
Injector guice = Guice.createInjector(
new AppModule(args),
new SpincastValidationPluginGuiceModule(IAppRequestContext.class)
// other modules...
);
... or by using the install(...) method from your custom Guice module:
public class AppModule extends SpincastDefaultGuiceModule {
//...
@Override
protected void configure() {
super.configure();
install(new SpincastValidationPluginGuiceModule(getRequestContextType()));
// other modules...
}
// ...
}
When this is in place, you can create validator classes.
For example, a UserValidator, to validate a user:
public class UserValidator extends SpincastValidatorBase<IUser> {
//...
@Override
protected void validate() {
// The validation rules go here.
}
}
A validator extends the SpincastValidatorBase base class, and parameterizes it
with the type of the objects to validate. It overrides the validate()
method to define the validation rules. We'll see an example of this in the
following Usage section.
You also have to bind a assisted factory for your new validator.
A factory is required because
a validator is stateful, it keeps informations about the current object being
validated. Therefore a validator can't be reused and a new instance must
be created for each object to validate, using its associated factory.
public class AppModule extends SpincastDefaultGuiceModule {
public AppModule(String[] mainArgs) {
super(mainArgs);
}
@Override
protected void configure() {
super.configure();
install(new SpincastValidationPluginGuiceModule(getRequestContextType()));
install(new FactoryModuleBuilder().implement(IValidator.class, UserValidator.class)
.build(new TypeLiteral<IValidatorFactory<IUser>>(){}));
//...
}
//...
}
Finally, you can then inject the IValidatorFactory<IUser> where you need to
create validators for users. Let's see an example...
In this example, we have a UserController controller containing a addUser(...)
route handler that receives POST requests to add new users:
public class UserController implements IUserController {
private final IUserService userService;
@Inject
public UserController(IUserService userService) {
this.userService = userService;
}
protected IUserService getUserService() {
return this.userService;
}
@Override
public void addUser(IAppRequestContext context) {
IUser user = context.request().getJsonBody(User.class);
user = getUserService().addUser(user);
context.response().sendJson(user);
}
}
Explanation :
IUser object. If the body is not valid Json or if the Json
can't be properly deserialized to a User object, an exception is thrown.
IUserService service. As we'll see, the validation of the user
object will be done there!
id that our system attributed to the new user.
Let's now have a look at the IUserService service, where the user object is validated:
public class UserService implements IUserService {
private final IUserRepository userRepository;
private final IValidatorFactory<IUser> userValidatorFactory;
@Inject
public UserService(IUserRepository userRepository,
IValidatorFactory<IUser> userValidatorFactory) {
this.userRepository = userRepository;
this.userValidatorFactory = userValidatorFactory;
}
protected IUserRepository getUserRepository() {
return this.userRepository;
}
protected IValidatorFactory<IUser> getUserValidatorFactory() {
return this.userValidatorFactory;
}
@Override
public IUser addUser(IUser user) {
IValidator userValidator = getUserValidatorFactory().create(user);
if(!userValidator.isValid()) {
String errorMessage = "The user contains errors:\n\n";
errorMessage += userValidator.getErrorsFormatted(FormatType.PLAIN_TEXT);
throw new PublicException(errorMessage);
}
return getUserRepository().addUser(user);
}
}
Explanation :
IValidatorFactory<IUser>
factory, the object that creates user validators.
user object that we want to validate.
userValidator.isValid(), we
can know if the validation was successful or not.
PublicException exception,
the error message will be available to the end user. Which is desired, since we want to inform
him of what went wrong.
id on the saved object and
will return this updated object.
Here is what the validator itself would look like:
public class UserValidator extends SpincastValidatorBase<IUser> {
@AssistedInject
public UserValidator(@Assisted IUser user,
IValidationErrorFactory validationErrorFactory,
ISpincastValidationConfig spincastBeanValidationConfig,
IJsonManager jsonManager,
IXmlManager xmlManager) {
super(user,
validationErrorFactory,
spincastBeanValidationConfig,
jsonManager,
xmlManager);
}
@Override
protected void validate() {
// The name of the user can be null.
validateNotNull("name", getObjToValidate().getName());
// The password of the user should be at least 12 characters long.
validateMinLength("password", getObjToValidate().getPassword(), 12);
// A custom validation
if("Stromgol".equalsIgnoreCase(getObjToValidate().getName())) {
addError("name",
"APP_USER_VALIDATION_ERROR_STROMGOL",
"I know Stromgol very well and you are not him!");
}
}
}
Explanation :
validate()
method, which is where we declare the validation rules.
name is
not null. Validation rules may use methods provided by the SpincastValidatorBase
base class, like validateNotNull(...) in this case. Each of those validation rule
at least takes a name representing the field that
is validated on the target object, and the value of the field.
password has
at least 12 characters.
addError(...) method. A validation error includes the name of the invalid field,
a type representing the error, and an error message.
Finally, don't forget to bind the assisted factory
for your validator, in your custom Guice module. This is what makes possible its injection in the
IUserService:
public class AppModule extends SpincastDefaultGuiceModule {
public AppModule(String[] mainArgs) {
super(mainArgs);
}
@Override
protected void configure() {
super.configure();
install(new SpincastValidationPluginGuiceModule(getRequestContextType()));
install(new FactoryModuleBuilder().implement(IValidator.class, UserValidator.class)
.build(new TypeLiteral<IValidatorFactory<IUser>>(){}));
//...
}
//...
}
Every validator implements the IValidator interface which provides
those methods:
boolean isValid()
void revalidate()
Map<String, List<IValidationError>> getErrors()
List<IValidationError> getErrors(String fieldName)
String getErrorsFormatted(FormatType formatType)
String getErrorsFormatted(String fieldName, FormatType formatType)
By extending SpincastValidatorBase, your validators get access to some default validation
methods. As we will see in the
next section, we can also define custom validation methods.
The default validation methods all return false if a validation error occures,
or true if the validation is successful:
boolean validateNotNull(String fieldName, Object fieldValue)
boolean validateNotNull(String fieldName, Object fieldValue, String errorMessage)
boolean validateNotBlank(String fieldName, String fieldValue)
boolean validateNotBlank(String fieldName, String fieldValue, String errorMessage)
boolean validateMinLength(String fieldName, String fieldValue, int minLength)
boolean validateMinLength(String fieldName, String fieldValue, int minLength, String errorMessage)
boolean validateMaxLength(String fieldName, String fieldValue, int maxLength)
boolean validateMaxLength(String fieldName, String fieldValue, int maxLength, String errorMessage)
boolean validateMinSize(String fieldName, Integer fieldValue, int minSize)
boolean validateMinSize(String fieldName, Integer fieldValue, int minSize, String errorMessage)
boolean validateMaxSize(String fieldName, Integer fieldValue, int maxSize)
boolean validateMaxSize(String fieldName, Integer fieldValue, int maxSize, String errorMessage)
boolean validatePattern(String fieldName, String fieldValue, String pattern)
boolean validatePattern(String fieldName, String fieldValue, String pattern, String errorMessage)
boolean validatePattern(String fieldName, String fieldValue, String pattern, boolean mustMatch)
boolean validatePattern(String fieldName, String fieldValue, String pattern, boolean mustMatch)
boolean validateEmail(String fieldName, String fieldValue)
boolean validateEmail(String fieldName, String fieldValue, String errorMessage)
In addition to those validation methods, SpincastValidatorBase
also provides methods to add your own errors when you define custom validation rules:
void addError(IValidationError error)
void addError(String fieldName, String errorType, String errorMessage)
name of the validated field, the type
of the error and its message.
void addErrors(List<IValidationError> errors)
In addition to the default validation methods provided by the SpincastValidatorBase
base class, you can define your own validation rules.
A custom rules is simply a validation followed by the registration of an error, if the validation fails.
For example, let's say we want to validate that a password contains at least one number. But let's also say we only want this error to be registered only if the password is not empty in the first place (which is also invalid)! In other words, we want the resulting validation of an empty password to contain only one error, "Can't be null, empty or only contain spaces.". We don't want two errors: "Can't be null, empty or only contain spaces." and "Must contain at least one number!":
public class UserValidator extends SpincastValidatorBase<IUser> {
@AssistedInject
public UserValidator(@Assisted IUser user,
IValidationErrorFactory validationErrorFactory,
ISpincastValidationConfig spincastBeanValidationConfig,
IJsonManager jsonManager,
IXmlManager xmlManager) {
super(user,
validationErrorFactory,
spincastBeanValidationConfig,
jsonManager,
xmlManager);
}
@Override
protected void validate() {
// Validates that the password is not blank.
// False is returned if the validation fails.
boolean passwordValid = validateNotBlank("password", getObjToValidate().getPassword());
// We only validate that the password contains at least one number
// if it is not blank (if the previous validation succeeded)!
if(passwordValid) {
if(getObjToValidate().getPassword().matches(".*\\d+.*")) {
passwordValid = addError("password",
"APP_USER_VALIDATION_ERROR_PASSWORD_AT_LEAST_ONE_NUMBER",
"Must contain at least one number!");
}
}
}
}
Explanation :
false
is returned.
field name,
an error type and an error message.