Enterprise Java

JAX-RS Bean Validation Error Message Internationalization

Introduction to Bean Validation

JavaBeans Validation (Bean Validation) is a new validation model available as part of Java EE 6 platform. The Bean Validation model is supported by constraints in the form of annotations placed on a field, method, or class of a JavaBeans component, such as a managed bean.

Several built-in constraints are available in the javax.validation.constraints package. The Java EE 6 Tutorial lists all the built-in constraints.

Constraints in Bean Validation are expressed via Java annotations:

public class Person {
    @NotNull
    @Size(min = 2, max = 50)
    private String name;
    // ...
}

Bean Validation and RESTful web services

JAX-RS 1.0 provides great support for extracting request values and binding them into Java fields, properties and parameters using annotations such as @HeaderParam, @QueryParam, etc. It also supports binding of request entity bodies into Java objects via non-annotated parameters (i.e., parameters that are not annotated with any of the JAX-RS annotations). Currently, any additional validation on these values in a resource class must be performed programmatically.

The next release, JAX-RS 2.0, includes a proposal to enable validation annotations to be combined with JAX-RS annotations. For example, given the validation annotation @Pattern, the following example shows how form parameters could be validated.

@GET
@Path("{id}")
public Person getPerson(
        @PathParam("id")
        @Pattern(regexp = "[0-9]+", message = "The id must be a valid number")
        String id) {
    return persons.get(id);
}

However, at the moment, the only solution is to use a proprietary implementation. What is presented next is a solution based on the RESTEasy framework from JBoss that complies with the JAX-RS specification and adds a RESTful validation interface through the annotation @ValidateRequest.

The exported interface allows us to create our own implementation. However, there is already one widely used and to which RESTEasy also provides a seamless integration. This implementation is Hibernate Validator.

This provider can be added to the project through the following Maven dependencies:

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <version>2.3.2.Final</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-hibernatevalidator-provider</artifactId>
    <version>2.3.2.Final</version>
</dependency>

Note:

without declaring the @ValidateRequest at class or method level, no validation will occur despite having applied constraint annotations on the methods, e.g. the example above.

@GET
@Path("{id}")
@ValidateRequest
public Person getPerson(
        @PathParam("id")
        @Pattern(regexp = "[0-9]+", message = "The id must be a valid number")
        String id) {
    return persons.get(id);
}

After applying the annotation, the parameter id will be automatically validated when a request is made.
You can of course validate entire entities instead of single fields by using the annotation @Valid.
We could for example have one method that accepts a Person object and validates it.

@POST
@Path("/validate")
@ValidateRequest
public Response validate(@Valid Person person) {
    // ...
}

Note:

By default, when validation fails an exception is thrown by the container and a HTTP 500 status is returned to the client. This default behavior can/should be overridden, allowing us to customize the Response that is returned to the client through exception mappers.

Internationalization

Until now we have been using the default or hard-coded error messages, but this is both a bad practice and not flexible at all. I18N is part of the Bean Validation specification and allows us to specify custom error messages using a resource property file. The default resource file name is ValidationMessages.properties and must include pairs of properties/values like:

person.id.pattern=The person id must be a valid number
person.name.size=The person name must be between {min} and {max} chars long

Note: {min}, {max} refer to properties of the constraint to which the message will be associated with.

Those defined messages can then be injected on the validation constraints as:

@POST
@Path("create")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createPerson(
        @FormParam("id")
        @Pattern(regexp = "[0-9]+", message = "{person.id.pattern}")
        String id,
        @FormParam("name")
        @Size(min = 2, max = 50, message = "{person.name.size}")
        String name) {
    Person person = new Person();
    person.setId(Integer.valueOf(id));
    person.setName(name);
    persons.put(Integer.valueOf(id), person);
    return Response.status(Response.Status.CREATED).entity(person).build();
}

To provide translations to other languages, one must create a new ValidationMessages_XX.properties file with the translated messages, where XX is the code of the language being provided.

Unfortunately Hibernate Validator provider doesn’t supports I18N based on a specific HTTP request. It does not take Accept-Language HTTP header into account and always uses the default Locale as provided by Locale.getDefault(). To be able to change the Locale using the Accept-Language HTTP header, a custom implementation must be provided.

Custom validator provider

The code below intends to address this issue and has been tested with JBoss AS 7.1.

The first thing to do is to remove the Maven resteasy-hibernatevalidator-provider dependency, since we are providing our own provider, and add Hibernate Validator dependency:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>4.2.0.Final</version>
</dependency>

Next create a custom message interpolator to adjust the default Locale used.

public class LocaleAwareMessageInterpolator extends
        ResourceBundleMessageInterpolator {

    private Locale defaultLocale = Locale.getDefault();

    public void setDefaultLocale(Locale defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    @Override
    public String interpolate(final String messageTemplate,
            final Context context) {
        return interpolate(messageTemplate, context, defaultLocale);
    }

    @Override
    public String interpolate(final String messageTemplate,
            final Context context, final Locale locale) {
        return super.interpolate(messageTemplate, context, locale);
    }
}

The next step is to provide a ValidatorAdapter. This interface was introduced to decouple RESTEasy from the real validation API.

public class RESTValidatorAdapter implements ValidatorAdapter {

    private final Validator validator;

    private final MethodValidator methodValidator;

    private final LocaleAwareMessageInterpolator interpolator = new LocaleAwareMessageInterpolator();

    public RESTValidatorAdapter() {
        Configuration<?> configuration = Validation.byDefaultProvider()
                .configure();
        this.validator = configuration.messageInterpolator(interpolator)
                .buildValidatorFactory().getValidator();
        this.methodValidator = validator.unwrap(MethodValidator.class);
    }

    @Override
    public void applyValidation(Object resource, Method invokedMethod,
            Object[] args) {
        // For the i8n to work, the first parameter of the method being validated must be a HttpHeaders
        if ((args != null) && (args[0] instanceof HttpHeaders)) {
            HttpHeaders headers = (HttpHeaders) args[0];
            List<Locale> acceptedLanguages = headers.getAcceptableLanguages();
            if ((acceptedLanguages != null) && (!acceptedLanguages.isEmpty())) {
                interpolator.setDefaultLocale(acceptedLanguages.get(0));
            }
        }

        ValidateRequest resourceValidateRequest = FindAnnotation
                .findAnnotation(invokedMethod.getDeclaringClass()
                        .getAnnotations(), ValidateRequest.class);

        if (resourceValidateRequest != null) {
            Set<ConstraintViolation<?>> constraintViolations = new HashSet<ConstraintViolation<?>>(
                    validator.validate(resource,
                            resourceValidateRequest.groups()));

            if (constraintViolations.size() > 0) {
                throw new ConstraintViolationException(constraintViolations);
            }
        }

        ValidateRequest methodValidateRequest = FindAnnotation.findAnnotation(
                invokedMethod.getAnnotations(), ValidateRequest.class);
        DoNotValidateRequest doNotValidateRequest = FindAnnotation
                .findAnnotation(invokedMethod.getAnnotations(),
                        DoNotValidateRequest.class);

        if ((resourceValidateRequest != null || methodValidateRequest != null)
                && doNotValidateRequest == null) {
            Set<Class<?>> set = new HashSet<Class<?>>();
            if (resourceValidateRequest != null) {
                for (Class<?> group : resourceValidateRequest.groups()) {
                    set.add(group);
                }
            }

            if (methodValidateRequest != null) {
                for (Class<?> group : methodValidateRequest.groups()) {
                    set.add(group);
                }
            }

            Set<MethodConstraintViolation<?>> constraintViolations = new HashSet<MethodConstraintViolation<?>>(
                    methodValidator.validateAllParameters(resource,
                            invokedMethod, args,
                            set.toArray(new Class<?>[set.size()])));

            if (constraintViolations.size() > 0) {
                throw new MethodConstraintViolationException(
                        constraintViolations);
            }
        }
    }
}

Warn: @HttpHeaders needs to be injected as the first parameter of the methods that are going to be validated:

@POST
@Path("create")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createPerson(
        @Context HttpHeaders headers,
        @FormParam("id")
        @Pattern(regexp = "[0-9]+", message = "{person.id.pattern}")
        String id,
        @FormParam("name")
        @Size(min = 2, max = 50, message = "{person.name.size}")
        String name) {
    Person person = new Person();
    person.setId(Integer.valueOf(id));
    person.setName(name);
    persons.put(id, person);
    return Response.status(Response.Status.CREATED).entity(person).build();
}

Finally, create the provider that will select the classes above to be used to validate Bean Validation constraints:

@Provider
public class RESTValidatorContextResolver implements
        ContextResolver<ValidatorAdapter> {

    private static final RESTValidatorAdapter adapter = new RESTValidatorAdapter();

    @Override
    public ValidatorAdapter getContext(Class<?> type) {
        return adapter;
    }
}

Mapping Exceptions

The Bean Validation API reports error conditions using exceptions of type javax.validation.ValidationException or any of its subclasses. Applications can supply custom exception mapping providers for any exception. A JAX-RS implementation MUST always use the provider whose generic type is the nearest superclass of the exception, with application-defined providers taking precedence over built-in providers.

The exception mapper may look like:

@Provider
public class ValidationExceptionMapper implements
        ExceptionMapper<MethodConstraintViolationException> {

    @Override
    public Response toResponse(MethodConstraintViolationException ex) {
        Map<String, String> errors = new HashMap<String, String>();
        for (MethodConstraintViolation<?> methodConstraintViolation : ex
                .getConstraintViolations()) {
            errors.put(methodConstraintViolation.getParameterName(),
                    methodConstraintViolation.getMessage());
        }
        return Response.status(Status.PRECONDITION_FAILED).entity(errors)
                .build();
    }
}

The above example shows the implementation of an ExceptionMapper that maps exceptions of type MethodConstraintViolationException. This exception is thrown by Hibernate Validator implementation when the validation of one or more parameters of a method annotated with the @ValidateRequest fails. This ensures that the client receives a formatted response instead of just the exception being propagated from the resource.

Source code

The source code used for this post is available on GitHub.
Warn: make sure you change the resource property file name to have the file ValidationMessages.properties (i.e., without any suffix) to map to the Locale as returned by Locale.getDefault().

Author: Samuel Santos is a Java and Open Source evangelist and a JUG leader of PT.JUG, the Portuguese Java User Group. He is the Technical Lead at Present Technologies in Coimbra, Portugal, where he is responsible for stimulating innovation, knowledge sharing, coaching and technology choices activities. Samuel is the author of the blog samaxes.com and tweets as @samaxes.
 

Reference:JAX-RS Bean Validation Error Message Internationalization from our JCG partner Samuel Santos at the Java Advent Calendar blog.

Samuel Santos

Java and Open Source evangelist, JUG leader and Web advocate for web standards and semantic technologies.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Theresa
Theresa
10 years ago

Hi,
thanks for this very good article. I tested it and it worked well. Unfortunately I’m working with resteasy and EJBs and runned in the following error:

https://github.com/psakar/resteasy-923
https://issues.jboss.org/browse/RESTEASY-923

Do you have any suggestions or recommendations how to handle this? I need so ‘save’ the locale not only for bean-validation but also vor generating internationalized error-messages (unfortunately the rest-client isn’t able to do this).

Thanks!

Back to top button