I’ve been using Jersey as a JAX-RS implementation for a little while now, and one thing that it could benefit from is the addition of an @Required
annotation for resource method parameters. Right now, when parameters are not provided by the client/request, they are simply set to null
, creating the need for duplicated null-checking in resource methods. An @Required
annotation would solve this issue and reduce code duplication.
This isn’t so much of an issue for @PathParam
parameters, (since you won’t even get to the proper resource method without a matching URI) but it does affect @HeaderParam
and @QueryParam
(among others) since they aren’t needed for Jersey to determine which resource method to invoke. By that definition, they are implicitly optional. There should be a way to make them required.
The behaviour of such a required annotation might be as follows:
- If the request does not have the parameter, then by default a
Response
withStatus.BAD_REQUEST
(HTTP 400) would be returned to the client. - Some way of customizing the HTTP response code and message should also be provided.
Right now, there’s not really an elegant way to make something like a @HeaderParam
required. Here are some solutions I’ve tried.
Attempt #1: Parameter classes
Parameter classes can be useful for transforming the single input of a parameter into a single output, and also for verifying that the input parameter value is valid. This can be useful for ensuring that an input parameter can be converted into a specific object, or that it matches a specific format.
As an example, consider the CsvListParam
class. This class takes a comma-separated list as a parameter, and returns a List<String>
comprising each entry in the list. It does an additional check to ensure that the input is not blank, according to StringUtils.isBlank(). If it is blank, a 400 Bad Request response is returned to the client via the WebApplicationException
thrown.
Note: The following examples use the AbstractParam
class from Coda Hale’s article.
public class CsvListParam extends AbstractParam<List<String>> {
public CsvListParam(String param) throws WebApplicationException {
super(param);
}
@Override
protected List<String> parse(final String suppliedStringValue) throws Throwable {
if (StringUtils.isBlank(suppliedStringValue)) {
throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).build());
}
return Arrays.asList(StringUtils.split(suppliedStringValue, ","));
}
}
@GET
@Path("test")
public Response test(@HeaderParam("X-TEST") final CsvListParam param) {
final String output;
if (null != param) {
output = param.getValue().toString();
} else {
output = "param was null";
}
return Response.ok(output).build();
}
However, if the parameter is not present at all – for example, if this was obtained via the @HeaderParam
annotation and the header was not present – then the code in the parameter class will not even be invoked by Jersey. Instead, the value will simply be null
. If we require this parameter, it results in nasty if-else null-checking code in each of our resource methods that requires the parameter.
We need something that gets rid of the necessary null-checking in each resource method.
Attempt #2: Injection Providers
Coda Hale provides another brilliant example how to use Injection Providers in Jersey. Basically, with Injection Providers, you can do everything that you could do with Parameter classes, and more.
While Parameter classes are useful only for single-input to single-output mapping, Injection Providers can map multiple inputs to single or multiple outputs. This could allow you to take multiple values in the HTTP request and use them populate a single Bean, or use them to form some more complex single value. It also allows you to do validation, as throwing a WebApplicationException
from an Injection Provider will cause the contained Response
or status to be properly returned to the client.
So, we can achieve a similar effect using an Injection Provider. A first attempt at resolving the issue yields the CsvListProvider class:
@Provider
public class CsvListProvider extends AbstractInjectableProvider<CsvListParam> {
public CsvListProvider() {
super(CsvListParam.class);
}
@Override
public CsvListParam getValue(final HttpContext httpContext) {
final String suppliedStringValue = httpContext.getRequest().getHeaderValue("X-TEST");
return new CsvListParam(suppliedStringValue);
}
}
@GET
@Path("test")
public Response test(@Context final CsvListParam param) {
final String output = param.getValue().toString();
return Response.ok(output).build();
}
However, while this works as expected, it has the unfortunate side effect that the source of the parameter (an HTTP header of “X-TEST”) has to be specified in the Provider class rather than on the annotation. This isn’t ideal since we have to create a new Injection Provider class for each HTTP header we want to make required.
Further attempts
I have been trying to figure out a solution to this. One possible way might be to change the AbstractInjectableProvider to the following declaration:
public abstract class AbstractInjectableProvider<E, A extends Annotation> extends AbstractHttpContextInjectable<E> implements InjectableProvider<A, Type>
We could then define a custom annotation type to take the place of A
instead of always using @Context
. However, this doesn’t work, as we have no way of then obtaining any of the annotation’s values in the concrete Provider class. A solution like this would require changes in the core of Jersey to make it work, thus reducing the solution essentially the same as having an @Required
annotation as proposed above.
Conclusion
It seems like we need a proper solution to this via a change in Jersey. Evidently, others have come to the same conclusion, as there are at least two issues open for Jersey related to this.
I agree. I saw a similar suggestion for Scala-style validation which would be nice:
@QueryParam @NotNull @Size(min=2, max=20)
See the code example on following URL, I wrote a custom annotation that checks for mandatory and optional parameter and you just add that annotation on top of your webservice method. So no if else checks required.
http://softwarewizworld.blogspot.com/2014/03/extending-jersey-framework.html