Validating JAX-RS resource data with Bean Validation in Java EE 7 and WildFly

April 1st, 2014 by Samuel Santos Leave a reply »

I have already approached this subject twice in the past. First, on my post Integrating Bean Validation with JAX-RS in Java EE 6, describing how to use Bean Validation with JAX-RS in JBoss AS 7, even before this was defined in the Java EE Platform Specification. And later, on an article written for JAX Magazine and posteriorly posted on JAXenter, using the new standard way defined in Java EE 7 with Glassfish 4 server (the first Java EE 7 certified server).
Now that WildFly 8, previously know as JBoss Application Server, has finally reached the final version and has joined the Java EE 7 certified servers club, it’s time for a new post highlighting the specificities and differences between these two application servers, GlassFish 4 and WildFly 8.

Specs and APIs

Java EE 7 is the long-awaited major overhaul of Java EE 6. With each release of Java EE, new features are added and existing specifications are enhanced. Java EE 7 builds on top of the success of Java EE 6 and continues to focus on increasing developer productivity.

JAX-RS, the Java API for RESTful Web Services, is one of the fastest-evolving APIs in the Java EE landscape. This is, of course, due to the massive adoption of REST-based Web services and the increasing number of applications that consume those services.

This post will go through the steps required to configure REST endpoints to support a JavaScript client and to handle validation exceptions to send localized error messages to the client in addition to HTTP error status codes.

Source code

The source code accompanying this article is available on GitHub.

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 7 Tutorial contains a list with all those 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 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 not annotated with any of the JAX-RS annotations). However, prior to JAX-RS 2.0, any additional validation on these values in a resource class had to be performed programmatically.

The last release, JAX-RS 2.0, includes a solution to enable validation annotations to be combined with JAX-RS annotations.
The following example shows how path parameters can be validated using the @Pattern validation annotation:

@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);
}

Besides validating single fields, you can also validate entire entities with the @Valid annotation.
As an example, the method below receives a Person object and validates it:

@POST
public Response validatePerson(@Valid Person person) {
    // ...
}

Internationalization

In the previous example we used 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.notnull=The person id must not be null
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 the properties of the constraint to which the message will be associated with.

Once defined, these messages can then be injected on the validation constraints such as:

@POST
@Path("create")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createPerson(
        @FormParam("id")
        @NotNull(message = "{person.id.notnull}")
        @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();
}

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

Unfortunately, with some application servers, the default Validator provider doesn’t support i18n based on a specific HTTP request. They do not take Accept-Language HTTP header into account and always use the default Locale as provided by Locale.getDefault(). To be able to change the Locale using the Accept-Language HTTP header (which maps to the language configured in your browser options), you must provide a custom implementation.

Custom Validator provider

Although WildFly 8 correctly uses the Accept-Language HTTP header to choose the correct resource bundle, other servers like GlassFish 4 do not use this header. Therefore, for completeness and easier comparison with the GlassFish code (available under the same GitHub project), I’ve also implemented a custom Validator provider for WildFly.
If you want to see a GlassFish example, please visit Integrating Bean Validation with JAX-RS on JAXenter.

1. Add RESTEasy dependency to Maven

WildFly uses RESTEasy, the JBoss implementation of the JAX-RS specification.
RESTEasy dependencies are required for the Validator provider and Exception Mapper discussed later on in this post. Lets add it to Maven:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-bom</artifactId>
            <version>3.0.6.Final</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxrs</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-validator-provider-11</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

2. Create a ThreadLocal to store the Locale from the Accept-Language HTTP header

ThreadLocal variables differ from their normal counterparts in that each thread that accesses one has its own, independently initialized copy of the variable.

/**
 * {@link ThreadLocal} to store the Locale to be used in the message interpolator.
 */
public class LocaleThreadLocal {

    public static final ThreadLocal<Locale> THREAD_LOCAL = new ThreadLocal<Locale>();

    public static Locale get() {
        return (THREAD_LOCAL.get() == null) ? Locale.getDefault() : THREAD_LOCAL.get();
    }

    public static void set(Locale locale) {
        THREAD_LOCAL.set(locale);
    }

    public static void unset() {
        THREAD_LOCAL.remove();
    }
}

3. Create a request filter to read the Accept-Language HTTP header

The request filter is responsible for reading the first language sent by the client in the Accept-Language HTTP header and store the Locale in our ThreadLocal:

/**
 * Checks whether the {@code Accept-Language} HTTP header exists and creates a {@link ThreadLocal} to store the
 * corresponding Locale.
 */
@Provider
public class AcceptLanguageRequestFilter implements ContainerRequestFilter {

    @Context
    private HttpHeaders headers;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        if (!headers.getAcceptableLanguages().isEmpty()) {
            LocaleThreadLocal.set(headers.getAcceptableLanguages().get(0));
        }
    }
}

4. Create a custom message interpolator to enforce a specific Locale

Next create a custom message interpolator to enforce a specific Locale value by bypassing or overriding the default Locale strategy:

/**
 * Delegates to a MessageInterpolator implementation but enforces a given Locale.
 */
public class LocaleSpecificMessageInterpolator implements MessageInterpolator {

    private final MessageInterpolator defaultInterpolator;

    public LocaleSpecificMessageInterpolator(MessageInterpolator interpolator) {
        this.defaultInterpolator = interpolator;
    }

    @Override
    public String interpolate(String message, Context context) {
        return defaultInterpolator.interpolate(message, context, LocaleThreadLocal.get());
    }

    @Override
    public String interpolate(String message, Context context, Locale locale) {
        return defaultInterpolator.interpolate(message, context, locale);
    }
}

5. Configure the Validator provider

RESTEasy obtains a Bean Validation implementation by looking for a Provider implementing ContextResolver<GeneralValidator>.
To configure a new Validation Service Provider to use our custom message interpolator add the following:

/**
 * Custom configuration of validation. This configuration can define custom:
 * <ul>
 * <li>MessageInterpolator - interpolates a given constraint violation message.</li>
 * <li>TraversableResolver - determines if a property can be accessed by the Bean Validation provider.</li>
 * <li>ConstraintValidatorFactory - instantiates a ConstraintValidator instance based off its class.
 * <li>ParameterNameProvider - provides names for method and constructor parameters.</li> *
 * </ul>
 */
@Provider
public class ValidationConfigurationContextResolver implements ContextResolver<GeneralValidator> {

    /**
     * Get a context of type {@code GeneralValidator} that is applicable to the supplied type.
     *
     * @param type the class of object for which a context is desired
     * @return a context for the supplied type or {@code null} if a context for the supplied type is not available from
     *         this provider.
     */
    @Override
    public GeneralValidator getContext(Class<?> type) {
        Configuration<?> config = Validation.byDefaultProvider().configure();
        BootstrapConfiguration bootstrapConfiguration = config.getBootstrapConfiguration();

        config.messageInterpolator(new LocaleSpecificMessageInterpolator(Validation.byDefaultProvider().configure()
                .getDefaultMessageInterpolator()));

        return new GeneralValidatorImpl(config.buildValidatorFactory(),
                bootstrapConfiguration.isExecutableValidationEnabled(),
                bootstrapConfiguration.getDefaultValidatedExecutableTypes());
    }
}

Mapping Exceptions

By default, when validation fails an exception is thrown by the container and a HTTP error is returned to the client.

Bean Validation specification defines a small hierarchy of exceptions (they all inherit from ValidationException) that could be thrown during initialization of validation engine or (for our case more importantly) during validation of input/output values (ConstraintViolationException). If a thrown exception is a subclass of ValidationException except ConstraintViolationException then this exception is mapped to a HTTP response with status code 500 (Internal Server Error). On the other hand, when a ConstraintViolationException is throw two different status code would be returned:

  • 500 (Internal Server Error)
    If the exception was thrown while validating a method return type.
  • 400 (Bad Request)
    Otherwise.

Unfortunately, WildFly instead of throwing the exception ConstraintViolationException for invalid input values, throws a ResteasyViolationException, which implements the ValidationException interface.
This behavior can be customized to allow us to add error messages to the response that is returned to the client:

/**
 * {@link ExceptionMapper} for {@link ValidationException}.
 * <p>
 * Send a {@link ViolationReport} in {@link Response} in addition to HTTP 400/500 status code. Supported media types
 * are: {@code application/json} / {@code application/xml} (if appropriate provider is registered on server).
 * </p>
 *
 * @see org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper The original WildFly class:
 *      {@code org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper}
 */
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {

    @Override
    public Response toResponse(ValidationException exception) {
        if (exception instanceof ConstraintDefinitionException) {
            return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
        }
        if (exception instanceof ConstraintDeclarationException) {
            return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
        }
        if (exception instanceof GroupDefinitionException) {
            return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
        }
        if (exception instanceof ResteasyViolationException) {
            ResteasyViolationException resteasyViolationException = ResteasyViolationException.class.cast(exception);
            Exception e = resteasyViolationException.getException();
            if (e != null) {
                return buildResponse(unwrapException(e), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
            } else if (resteasyViolationException.getReturnValueViolations().size() == 0) {
                return buildViolationReportResponse(resteasyViolationException, Status.BAD_REQUEST);
            } else {
                return buildViolationReportResponse(resteasyViolationException, Status.INTERNAL_SERVER_ERROR);
            }
        }
        return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
    }

    protected Response buildResponse(Object entity, String mediaType, Status status) {
        ResponseBuilder builder = Response.status(status).entity(entity);
        builder.type(MediaType.TEXT_PLAIN);
        builder.header(Validation.VALIDATION_HEADER, "true");
        return builder.build();
    }

    protected Response buildViolationReportResponse(ResteasyViolationException exception, Status status) {
        ResponseBuilder builder = Response.status(status);
        builder.header(Validation.VALIDATION_HEADER, "true");

        // Check standard media types.
        MediaType mediaType = getAcceptMediaType(exception.getAccept());
        if (mediaType != null) {
            builder.type(mediaType);
            builder.entity(new ViolationReport(exception));
            return builder.build();
        }

        // Default media type.
        builder.type(MediaType.TEXT_PLAIN);
        builder.entity(exception.toString());
        return builder.build();
    }

    protected String unwrapException(Throwable t) {
        StringBuffer sb = new StringBuffer();
        doUnwrapException(sb, t);
        return sb.toString();
    }

    private void doUnwrapException(StringBuffer sb, Throwable t) {
        if (t == null) {
            return;
        }
        sb.append(t.toString());
        if (t.getCause() != null && t != t.getCause()) {
            sb.append('[');
            doUnwrapException(sb, t.getCause());
            sb.append(']');
        }
    }

    private MediaType getAcceptMediaType(List<MediaType> accept) {
        Iterator<MediaType> it = accept.iterator();
        while (it.hasNext()) {
            MediaType mt = it.next();
            /*
             * application/xml media type causes an exception:
             * org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response
             * object of type: org.jboss.resteasy.api.validation.ViolationReport of media type: application/xml
             */
            /*if (MediaType.APPLICATION_XML_TYPE.getType().equals(mt.getType())
                    && MediaType.APPLICATION_XML_TYPE.getSubtype().equals(mt.getSubtype())) {
                return MediaType.APPLICATION_XML_TYPE;
            }*/
            if (MediaType.APPLICATION_JSON_TYPE.getType().equals(mt.getType())
                    && MediaType.APPLICATION_JSON_TYPE.getSubtype().equals(mt.getSubtype())) {
                return MediaType.APPLICATION_JSON_TYPE;
            }
        }
        return null;
    }
}

The above example is an implementation of the ExceptionMapper interface which maps exceptions of the type ValidationException. This exception is thrown by the Validator implementation when the validation fails. If the exception is an instance of ResteasyViolationException we send a ViolationReport in the response in addition to HTTP 400/500 status code. This ensures that the client receives a formatted response instead of just the exception being propagated from the resource.

The produced output looks just like the following (in JSON format):

{
    "exception": null,
    "fieldViolations": [],
    "propertyViolations": [],
    "classViolations": [],
    "parameterViolations": [
        {
            "constraintType": "PARAMETER",
            "path": "getPerson.id",
            "message": "The id must be a valid number",
            "value": "test"
        }
    ],
    "returnValueViolations": []
}

Running and testing

To run the application used for this article, build the project with Maven, deploy it into a WildFly 8 application server, and point your browser to http://localhost:8080/jaxrs-beanvalidation-javaee7/.

Alternatively, you can run the tests from the class PersonsIT which are built with Arquillian and JUnit. Arquillian will start an embedded WildFly 8 container automatically, so make sure you do not have another server running on the same ports.

Suggestions and improvements

  1. We are dependent on application server code in order to implement a custom Validator provider. On GlassFish 4 ContextResolver<ValidationConfig> needs to be implemented, while on WildFly 8 we need to implement ContextResolver<GeneralValidator>. Why not defined an interface on the Java EE 7 spec that both ValidationConfig and GeneralValidator must implement instead of relying on the application server specific code?
  2. Make WildFly 8 Embedded easier to use and configure with Maven. Currently, for it to be available to Arquillian, one needs to download the WildFly distribution (org.wildfly:wildfly-dist), unzip it into the target folder, and configure the system properties on Surefire/Failsafe Maven plugins:

    <systemPropertyVariables>
        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
        <jboss.home>${wildfly.home}</jboss.home>
        <module.path>${wildfly.home}/modules</module.path>
    </systemPropertyVariables>
    

    Whereas for Glassfish you just need to define the correct dependency (org.glassfish.main.extras:glassfish-embedded-all).

  3. Make RESTEasy a transitive dependency of WildFly Embedded. Having all the WildFly modules available on compile time just by defining a provided WildFly Embedded dependency would be a nice productive boost.
  4. It is currently not possible to use the option Run As >> JUnit Test on Eclipse since a system property named jbossHome must exist. This property is not read from Surefire/Failsafe configuration by Eclipse. Is there a workaround for this?
  5. When using RESTEasy default implementation of ExceptionMapper<ValidationException>, requesting the data in application/xml media type and having validation errors, will throw the following exception:

    org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure:
        Could not find MessageBodyWriter for response object of type:
            org.jboss.resteasy.api.validation.ViolationReport of media type:
                application/xml
    

    Update: issue RESTEASY-1054 created by Ron Sigal.

Don't be shellfish...Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedInPin on PinterestBuffer this pageEmail this to someone
Advertisement

9 comments

  1. Nice article!

    Have you considered instead of implementing a custom (dependent) ContextResolver to create a META-INF/validation.xml to configure the ValidationFactory?

    com.samaxes.javax.rs.validation.LocaleSpecificMessageInterpolator

    cfr http://docs.jboss.org/hibernate/validator/5.0/reference/en-US/html/chapter-xml-configuration.html#section-configuration-validation-xml

  2. Ron Sigal says:

    Hi Samuel,

    Resteasy 3.x is intended to be compatible with JBoss Application Server 7 (which ships with Hibernate Validator 4.x, based on Bean Validation 1.0) and WildFly 8 (which ships with Hibernate Validator 5.x, based on Bean Validation 1.1). Method validation was an extension in Hibernate Validator 4.x, and the two versions have somewhat different APIs. The GeneralValidator interface was introduced to mediate the two APIs.
    Validation is called from the core Resteasy module, but we didn’t want to force people using Resteasy in standalone mode to depend on validation jars. ResteasyConstraintViolation was introduced to avoid validation dependencies.
    The failure to marshal ViolationReport by the JAXB provider is indeed a Resteasy bug: https://issues.jboss.org/browse/RESTEASY-1054. It will be fixed in the next release. Thank you for the catch.

    -Ron Sigal

    • Thanks Ron for the valuable feedback!

      The GeneralValidator interface was introduced to mediate the two APIs.

      I still do believe that the spec should define an interface so developers can do something like ContextResolver in any Java EE 7 compliant server. Don’t you agree?

      ResteasyConstraintViolation was introduced to avoid validation dependencies.

      But why not implement the ConstraintViolationException interface instead of ValidationException? That would allow developers to simply do if (exception instanceof ConstraintViolationException) and have it work across all Java EE 7 certified servers.

      • Ron Sigal says:

        Hi Samuel,

        re: I still do believe that the spec should define an interface so developers can do something like ContextResolver in any Java EE 7 compliant server. Don’t you agree?

        Well, GeneralValidator solved a problem specific to JBoss, in that I wanted Resteasy validation to work in both Application Server 7 and Wildfly 8. Also, GeneralValidator is an SPI interface, in case someone wanted to used something other than Hibernate Validator. So it’s not something most developers will use.

        re: But why not implement the ConstraintViolationException interface instead of ValidationException? That would allow developers to simply do if (exception instanceof ConstraintViolationException) and have it work across all Java EE 7 certified servers.

        Maybe that would have made sense. Note, though, that we don’t actually return a ResteasyViolationException. If “application/xml” is accepted, we return a ViolationReport. If not, we return a string from which either ResteasyViolationException or (as of the next release) ViolationReport can be created.

        Regarding the failure to marshal ViolationReport, it turns out that I didn’t have the JAXB annotations on the classpath. I’ve attached updated versions of resteasy-jboss-modules-3.0.8.Final.zip and resteasy-jboss-modules-wf8-3.0.8.Final.zip to https://issues.jboss.org/browse/RESTEASY-1054. Feedback is welcome.

        -Ron

  3. Ron Sigal says:

    Thank you for the feedback, Samuel. The fix will go into the next Resteasy release.

  4. Charles Queiroz says:

    Muito bom o artigo… Eu gostaria de saber, se você sabe alguma forma de fazer a mesma coisa, mas integrando com JSF.

    Por exemplo, tenho um sistema: Java EE7 + Wildfly + JSF 2.2 + Beans Validator (Hibernate Validator). Eu implementei a internacionalização, e está funcionando na interface sem problemas, mas as mensagens de validação ainda aparecem em inglês, mesmo eu mudando o idioma para português. Você sabe como eu posso resolver isso?

Leave a Reply