Magic beans

Integrating Bean Validation with JAX-RS

In this article originally published in the August edition of JAX Magazine, 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.

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/





 


 

 

Samuel Santos

What do you think?

JAX Magazine - 2014 - 03 Exclucively for iPad users JAX Magazine on Android

Comments

Latest opinions