All Downloads are FREE. Search and download functionalities are using the official Maven repository.

templates.plugins.spincast-validation.spincast-validation.html Maven / Gradle / Ivy

There is a newer version: 0.9.28
Show newest version
{#==========================================
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 %}

Overview

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.

Installation

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, 
                                                IAppWebsocketContext.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(), 
                                                        getWebsocketContextType()));
        // 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 the factory.

public class AppModule extends SpincastDefaultGuiceModule {

    public AppModule(String[] mainArgs) {
        super(mainArgs);
    }

    @Override
    protected void configure() {
        super.configure();

        install(new SpincastValidationPluginGuiceModule(getRequestContextType(), 
                                                        getWebsocketContextType()));

        install(new FactoryModuleBuilder().implement(IValidator.class, UserValidator.class)
                                          .build(new TypeLiteral<IValidatorFactory<IUser>>(){}));

        //...
    }

    //...
}

You can then inject the IValidatorFactory<IUser> where you need to create validators for users. Let's see an example...

Usage

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().sendJsonObj(user);
    }
}

Explanation :

  • 17 : We deserialize the body of a request we receive to a 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.
  • 18 : We pass the new user to add to a IUserService service. As we'll see, the validation of the user object will be done there!
  • 20 : If the user is valid, and therefore saved in our system, we can return a new serialized version of it. This new version now contains an unique 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 :

  • 8 : We inject the IValidatorFactory<IUser> factory, the object that creates user validators.
  • 24 : We create a validator instance, by passing as a parameter the user object that we want to validate.
  • 25 : By calling userValidator.isValid(), we can know if the validation was successful or not.
  • 27-28 : If some errors occured, we can access each error separately, but we can also ask the validator to give us a formatted listing of all the errors. Here, we ask for a plain text listing, and we use it to build the message we're going to send in an exception.
  • 30 : By throwing a 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.
  • 33 : In the other hand, if the validation is successful, we add the user to our system! In general, a repository will add the new object to the appropriated data source, will set the generated 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,
                         SpincastValidatorBaseDeps spincastValidatorBaseDeps) {
        super(user, spincastValidatorBaseDeps);
    }

    @Override
    protected void validate() {

        // The name of the user can't 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 :

  • 9-10 : We override the abstract validate() method, which is where we declare the validation rules.
  • 13 : We validate that the user's 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. Also note that the getObjToValidate() method returns the object to validate (the user in this case).
  • 16 : We validate that the user's password contains at least 12 characters.
  • 19-23 : We add a custom validation rule. Here, the user can't be named "Stromgol". If the custom rule fails, we register an error using the 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(), 
                                                        getWebsocketContextType()));

        install(new FactoryModuleBuilder().implement(IValidator.class, UserValidator.class)
                                          .build(new TypeLiteral<IValidatorFactory<IUser>>(){}));

        //...
    }

    //...
}

The base methods

Every validator implements the IValidator interface which provides those methods:

  • boolean isValid()
    Is the object valid? Was the validation successful?
  • void revalidate()
    Revalidates the object (it may have been modified!). The result of the validation is cached, so it will always be the same except if this method is called to clear it.
  • Map<String, List<IValidationError>> getErrors()
    Gets the validation errors. The keys are the names of invalid fields.
  • List<IValidationError> getErrors(String fieldName)
    Gets the validation errors for the specified field.

  • String getErrorsFormatted(FormatType formatType)
    Gets the formatted errors, if there are any.
    @param formatType The type of output for the errors (Text, HTML, Json or XML).
    @return the formatted errors or null if there are no validation errors.
  • String getErrorsFormatted(String fieldName, FormatType formatType)
    Gets the formatted errors for the specified field, if there are any.
    @param fieldName The field to get errors for.
    @param formatType The type of output for the errors (Text, HTML, Json or XML).
    @return the formatted errors or null if there are no validation errors.

The default validation methods

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)
    Validates that a field is not null.
  • boolean validateNotNull(String fieldName, Object fieldValue, String errorMessage)
    Validates that a field is not null, and allows a custom error message.
  • boolean validateNotBlank(String fieldName, String fieldValue)
    Validates that a field is not blank (null, empty or contain only spaces).
  • boolean validateNotBlank(String fieldName, String fieldValue, String errorMessage)
    Validates that a field is not blank (null, empty or contain only spaces), and allows a custom error message.
  • boolean validateMinLength(String fieldName, String fieldValue, int minLength)
    Validates that a field has a minimum length.
  • boolean validateMinLength(String fieldName, String fieldValue, int minLength, String errorMessage)
    Validates that a field has a minimum length, and allows a custom error message.
  • boolean validateMaxLength(String fieldName, String fieldValue, int maxLength)
    Validates that a field has a maximum length.
  • boolean validateMaxLength(String fieldName, String fieldValue, int maxLength, String errorMessage)
    Validates that a field has a maximum length, and allows a custom error message.
  • boolean validateMinSize(String fieldName, Integer fieldValue, int minSize)
    Validates that a field has a minimum size.
  • boolean validateMinSize(String fieldName, Integer fieldValue, int minSize, String errorMessage)
    Validates that a field has a minimum size, and allows a custom error message.
  • boolean validateMaxSize(String fieldName, Integer fieldValue, int maxSize)
    Validates that a field has a maximum size.
  • boolean validateMaxSize(String fieldName, Integer fieldValue, int maxSize, String errorMessage)
    Validates that a field has a maximum size, and allows a custom error message.
  • boolean validatePattern(String fieldName, String fieldValue, String pattern)
    Validates that a field matches a pattern.
  • boolean validatePattern(String fieldName, String fieldValue, String pattern, String errorMessage)
    Validates that a field matches a pattern, and allows a custom error message.
  • boolean validatePattern(String fieldName, String fieldValue, String pattern, boolean mustMatch)
    Validates that a field matches a pattern or not.
    @param mustMatch if true, the field value must match the pattern, if false, it must not match it.
  • boolean validatePattern(String fieldName, String fieldValue, String pattern, boolean mustMatch)
    Validates that a field matches a pattern or not, and allows a custom error message.
    @param mustMatch if true, the field value must match the pattern, if false, it must not match it.
  • boolean validateEmail(String fieldName, String fieldValue)
    Validates that a field is a valid email address.
  • boolean validateEmail(String fieldName, String fieldValue, String errorMessage)
    Validates that a field is a valid email address, and allows a custom error message.

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)
    Adds a validation error object directly.
  • void addError(String fieldName, String errorType, String errorMessage)
    Adds a validation error by specifying the name of the validated field, the type of the error and its message.
  • void addErrors(List<IValidationError> errors)
    Adds validation errors directly.

Custom validation

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 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,
                         SpincastValidatorBaseDeps spincastValidatorBaseDeps) {
        super(user, spincastValidatorBaseDeps);
    }

    @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 :

  • 14 : We first validate that the password is not blank. If it is blank, the validation fails and false is returned.
  • 18 : We only continue validating the password if it is not blank in the first place. Of course, we could still run the other validations and have more errors displayed to the end user: this is up to you.
  • 19 : Is the password is not empty, we validate that it contains at least one number.
  • 20-22 : If our custom validation fails, we register an validation error. A validation error is composed of a field name, an error type and an error message.

{% endblock %}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy