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]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;
// ...
}
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:
<strong>Listing 1</strong>
@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:
<strong>Listing 2</strong>
@POST
@Path("validate")
@ValidateRequest
public Response validate(@Valid Person person) {
// ...
}
End
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.
<strong>Listing 3</strong>
@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.
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.
<strong>Listing 4</strong>
<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.
<strong>Listing 5</strong>
/**
* {@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:
<strong>Listing 6</strong>
**
* 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
<strong>Listing 7</strong>
/**
* 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.
<strong>Listing 8</strong>
/**
* 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
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:
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.
<strong>Listing 10</strong>
/**
* 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"}
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/