Search code examples
javaspringrestjax-rscxf

Limiting the values of Query Params JAX-RS with CXF as implementation


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.


Solution

  • Annotation inheritance

    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 targets

    Bear in mind that the @QueryParam annotation can be applied to:

    • Resource method parameters
    • Resource class fields
    • Resource class bean properties

    Hence a manual validation can be tricky.

    Use Bean Validation

    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) {
            ...
        } 
    }