Magic beans

Integrating Bean Validation with JAX-RS

SamuelSantos
beans

Samuel Santos takes a look at one of the most rapidly changing APIs in the enterprise version of Java – JAX-RS.

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 [1]. This is, of
course, due to the massive adoption of REST-based Web services and
the increasing number of applications that consume those
services.

In this article, we 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. The source code
accompanying this article is available on GitHub [2] 

Introduction to Bean Validation

JavaBeans Validation (Bean Validation) is a new
validation model available as part of Java EE 7 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
lists all those built-in constraints [3].

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 that are not
annotated with any of the JAX-RS annotations). However, until
JAX-RS 2.0, any additional validation on these values in a resource
class would have to be performed programmatically.

The last 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, Listing 1 shows how path parameters can be
validated:

Listing 1

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

}

End

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, like Listing 2:

Listing 2

@POST

@Path("validate")

@ValidateRequest

public Response validate(@Valid Person person) {

// ...

}

End

Internationalization

In Listings 1 and 2, we have 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.

Those defined messages can then be injected on
the validation constraints as shown in Listing 3.

Listing 3

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

}

End

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, the default Validator provider
doesn’t support i18n based on a specific HTTP request. It does not
take Accept-Language HTTP header into account either and always
uses the default Locale, as provided by Locale.getDefault(). To be
able to change the Locale using the Accept-Language HTTP header
(e.g., changing the language in the browser options), a custom
implementation must be provided.

Custom Validator provider

The code below intends to address this problem
and has been tested with GlassFish 4. The first thing to do is to
add the GlassFish dependency glassfish-embedded-all to Maven, as
shown in Listing 4.

Listing 4

<dependency>

<groupId>org.glassfish.main.extras</groupId>

<artifactId>glassfish-embedded-all</artifactId>

<version>4.0</version>

<scope>provided</scope>

</dependency>

End

Next, create a ThreadLocal to store the Locale from the
Accept-Language HTTP header, as shown in Listing 5. ThreadLocal
variables differ from their normal counterparts, in that each
thread that accesses one has its own independently initialized copy
of the variable.

Listing 5

/**

* {@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();

}

}

End

Following this, create a request filter to read the
Accept-Language HTTP header, like in Listing 6. 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:

Listing 6

**

* 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 {

LocaleThreadLocal.set(headers.getAcceptableLanguages().get(0));

}

End

Next, create a custom message
interpolator to enforce a specific Locale value by bypassing or
overriding the default Locale strategy. This is shown in Listing
7

Listing 7

/**

* Delegates to a MessageInterpolator implementation but enforces a given Locale.

*/

public class LocaleSpecificMessageInterpolator implements MessageInterpolator {



private final MessageInterpolator defaultInterpolator;



private final Locale defaultLocale;



public LocaleSpecificMessageInterpolator(MessageInterpolator interpolator, Locale locale) {

this.defaultInterpolator = interpolator;

this.defaultLocale = locale;

}



/**

* Enforces the locale passed to the interpolator.

*/

@Override

public String interpolate(String message, Context context) {

return defaultInterpolator.interpolate(message, context, this.defaultLocale);

}



// no real use, implemented for completeness

@Override

public String interpolate(String message, Context context, Locale locale) {

return defaultInterpolator.interpolate(message, context, locale);

}

}

End

GlassFish uses Jersey, the reference implementation for JAX-RS,
which allows customization of the Validator used in validation of
resource classes/methods using ValidationConfig class and exposing
it via ContextResolver<T> mechanism [4]. Configure the
Validator to use our custom message interpolator, like in Listing
8.

Listing 8

/**

* 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<ValidationConfig> {



private static final Logger LOGGER = Logger.getLogger(ValidationConfigurationContextResolver.class.getName());



@Context

private HttpHeaders headers;



/**

* Get a context of type {@code ValidationConfiguration} 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 ValidationConfig getContext(Class<?> type) {

final ValidationConfig config = new ValidationConfig();



config.setMessageInterpolator(new LocaleSpecificMessageInterpolator(Validation.byDefaultProvider().configure()

.getDefaultMessageInterpolator(), headers.getAcceptableLanguages().get(0)));



return config;

}

}

End

Mapping Exceptions

When validation fails, an exception is thrown by
the container by default 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 thrown, two different status codes
could be returned:

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

This behavior can be customized to allow us to
add error messages to the response that is returned to the client,
like in Listing 9.

/**
 * {@link ExceptionMapper} for {@link ValidationException}.
 * <p>
 * Send a list of {@link ValidationError} instances 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>
 */
@javax.ws.rs.ext.Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {

    private static final Logger LOGGER = Logger.getLogger(ValidationExceptionMapper.class.getName());

    @Context
    private Configuration config;

    @Context
    private Provider<Request> request;

    @Override
    public Response toResponse(final ValidationException exception) {
        if (exception instanceof ConstraintViolationException) {
            LOGGER.log(Level.FINER, "Following ConstraintViolations has been encountered.", exception);
            final ConstraintViolationException cve = (ConstraintViolationException) exception;
            final Response.ResponseBuilder response = Response.status(getStatus(cve));

            // Entity
            final List<Variant> variants = Variant.mediaTypes(MediaType.APPLICATION_XML_TYPE,
                    MediaType.APPLICATION_JSON_TYPE).build();
            final Variant variant = request.get().selectVariant(variants);
            if (variant != null) {
                response.type(variant.getMediaType());
            }
            response.entity(
                    new GenericEntity<List<ValidationError>>(
                            getEntity(cve.getConstraintViolations()),
                            new GenericType<List<ValidationError>>() {}.getType()
                    )
            );

            return response.build();
        } else {
            LOGGER.log(Level.WARNING, "Unexpected Bean Validation problem.", exception);

            return Response.serverError().entity(exception.getMessage()).build();
        }
    }

    private List<ValidationError> getEntity(final Set<ConstraintViolation<?>> violations) {
        final List<ValidationError> errors = new ArrayList<ValidationError>();

        for (final ConstraintViolation<?> violation : violations) {
            errors.add(new ValidationError(getInvalidValue(violation.getInvalidValue()), violation.getMessage(),
                    violation.getMessageTemplate(), getPath(violation)));
        }

        return errors;
    }

    private String getInvalidValue(final Object invalidValue) {
        if (invalidValue == null) {
            return null;
        }

        if (invalidValue.getClass().isArray()) {
            return Arrays.toString((Object[]) invalidValue);
        }

        return invalidValue.toString();
    }

    private Response.Status getStatus(final ConstraintViolationException exception) {
        return getResponseStatus(exception.getConstraintViolations());
    }

    private Response.Status getResponseStatus(final Set<ConstraintViolation<?>> constraintViolations) {
        final Iterator<ConstraintViolation<?>> iterator = constraintViolations.iterator();

        if (iterator.hasNext()) {
            return getResponseStatus(iterator.next());
        } else {
            return Response.Status.BAD_REQUEST;
        }
    }

    private Response.Status getResponseStatus(final ConstraintViolation<?> constraintViolation) {
        for (final Path.Node node : constraintViolation.getPropertyPath()) {
            final ElementKind kind = node.getKind();

            if (ElementKind.RETURN_VALUE.equals(kind)) {
                return Response.Status.INTERNAL_SERVER_ERROR;
            }
        }

        return Response.Status.BAD_REQUEST;
    }

    private String getPath(final ConstraintViolation<?> violation) {
        final String leafBeanName = violation.getLeafBean().getClass().getSimpleName();
        final String leafBeanCleanName = (leafBeanName.contains("$")) ? leafBeanName.substring(0,
                leafBeanName.indexOf("$")) : leafBeanName;
        final String propertyPath = violation.getPropertyPath().toString();

        return leafBeanCleanName + (!"".equals(propertyPath) ? '.' + propertyPath : "");
    }
}

Listing 9 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 ConstraintViolationException we send a list of
ValidationError instances 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 ValidationError class is a very simple
validation error entity, shown in Listing 10.

Listing 10

/**

* Default validation error entity to be included in {@code Response}.

*/

@XmlRootElement

public final class ValidationError {



private String invalidValue;

private String message;

private String messageTemplate;

private String path;



public ValidationError() {

}



public ValidationError(final String invalidValue, final String message, final String messageTemplate,

final String path) {

this.invalidValue = invalidValue;

this.message = message;

this.messageTemplate = messageTemplate;

this.path = path;

}



// Getters and Setters...

}

Ends



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



{"invalidValue":"test","message":"The id must be a valid number","messageTemplate":"The id must be a valid number","path":"Persons.getPerson.id"}

Running and testing

To run the application used for this article,
build the project with Maven, deploy it into a GlassFish 4
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 [5] [6].
Arquillian will start an embedded GlassFish container
automatically, so make sure you do not have another server running
on the same ports.

References

[1] https://jax-rs-spec.java.net/

[2]

https://github.com/samaxes/jaxrs-beanvalidation-javaee7

[3]

http://docs.oracle.com/javaee/7/tutorial/doc/jsf-develop004.htm#GKAGK

[4] https://jersey.java.net/

[5] http://arquillian.org/

[6] http://junit.org/

 


Author
Comments
comments powered by Disqus