I have a use case where I need to limit the values that can be passed as the query param.
@Path("/foo")
public interface Foo {
@GET
@Path("/details/id/{id}")
void getFooDetails(@PathParam("id") String id, @QueryParam("sort") String sortDirection);
}
public class FooImpl {
public void getFooDetails(String id, String sortDir) {
//Implementation
}
}
In the above example, I want to restrict the value of query param sort
that can be passed via the API to ASC, DESC
.
Is there any existing CXF annotation which I can use to restrict the values on a parameter? I haven't found any and so I tried the following solution.
My Approach:
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ValueSet {
String[] allowedValues();
}
The modified interface looks like this.
@Path("/foo")
public interface Foo {
@GET
@PathParam("/details/id/{id}")
void getFooDetails(@PathParam("id") String id, @QueryParam("sort") @ValueSet(allowedValues = {"ASC", "DESC"}) String sortDirection);
}
I wrote a CXF Interceptor which intercepts the API invocation. I used reflection to get a handle on FooImpl.getFooDetails
params. But the problem I faced is that the interceptor looks at FooImpl.getFooDetails method and doesn't find the annotations @QueryParam on the method params since @QueryParam is on the base method and the annotation is not inherited.
Interceptor implementation:
@Provider
public class ParamValidationInterceptor extends AbstractPhaseInterceptor<Message> {
public ParamValidationInterceptor() {
super(Phase.PRE_INVOKE);
super.addBefore(someInterceptor);
}
@Override
public void handleMessage(Message message) throws Fault {
UriInfo uriInfo = new UriInfoImpl(message);
MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
Method methodToInvoke = (Method) message.get("org.apache.cxf.resource.method");
Parameter[] parameters = methodToInvoke.getParameters();
for (Parameter parameter : parameters) {
if (parameter.isAnnotationPresent(ValueSet.class)) {
ValueSet valueSet = parameter.getAnnotation(ValueSet.class);
QueryParam queryParam = parameter.getAnnotation(QueryParam.class);
Object invokedVal = queryParams.get(queryParam.value());
String[] allowedValues = valueSet.allowedValues();
if (!Arrays.asList(allowedValues).contains(invokedVal)) {
throw new CustomException();
}
}
}
}
}
Can anyone suggest a way forward? It would be great if anyone can suggest an alternative approach.
P.S: I am using CXF as an implementation for JAX-RS and spring is used as a container.
Update:
Like @Cássio Mazzochi Molin and @Andy McCright suggested, I will go with @Pattern annotation. But I am curious to know why the JAX-RS annotations are not inherited from the interface although the spec says they will be inherited.
According to the section §3.6 Annotation Inheritance of the JAX-RS specification, it is recommended to always repeat annotations instead of relying on annotation inheritance.
Refer to this answer for the complete quote.
@QueryParam
can be applied to different targetsBear in mind that the @QueryParam
annotation can be applied to:
Hence a manual validation can be tricky.
For validation purposes, you should consider Bean Validation. Consider a @Pattern
annotation with the allowed values:
@Pattern(regexp = "ASC|DESC")
And just annotate your resource method parameter:
@GET
@Path("foo")
public Response getFoos(@QueryParam("sort")
@Pattern(regexp = "ASC|DESC") String sortDirection) {
...
}
If you prefer case insensitive values, use:
@Pattern(regexp = "ASC|DESC", flags = Pattern.Flag.CASE_INSENSITIVE)
If the given value is invalid, a ConstraintViolationException
will be thrown. To handle such exception and return a customized response, you can use an ExceptionMapper
:
@Provider
public class ConstraintViolationExceptionMapper
implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
...
}
}