Search code examples
javajerseyjersey-1.0

How to get resource method matched to URI before Jersey invokes it?


I'm trying to implement a ContainerRequestFilter that does custom validation of a request's parameters. I need to look up the resource method that will be matched to the URI so that I can scrape custom annotations from the method's parameters.

Based on this answer I should be able to inject ExtendedUriInfo and then use it to match the method:

public final class MyRequestFilter implements ContainerRequestFilter {

    @Context private ExtendedUriInfo uriInfo;

    @Override
    public ContainerRequest filter(ContainerRequest containerRequest) {

        System.out.println(uriInfo.getMatchedMethod());

        return containerRequest;
    }
}

But getMatchedMethod apparently returns null, all the way up until the method is actually invoked (at which point it's too late for me to do validation).

How can I retrieve the Method that will be matched to a given URI, before the resource method is invoked?


For those interested, I'm trying to roll my own required parameter validation, as described in JERSEY-351.


Solution

  • I figured out how to solve my problem using only Jersey. There's apparently no way to match a request's URI to the method that will be matched before that method is invoked, at least in Jersey 1.x. However, I was able to use a ResourceFilterFactory to create a ResourceFilter for each individual resource method - that way these filters can know about the destination method ahead of time.

    Here's my solution, including the validation for required query params (uses Guava and JSR 305):

    public final class ValidationFilterFactory implements ResourceFilterFactory {
    
        @Override
        public List<ResourceFilter> create(AbstractMethod abstractMethod) {
    
            //keep track of required query param names
            final ImmutableSet.Builder<String> requiredQueryParamsBuilder =
                    ImmutableSet.builder();
    
            //get the list of params from the resource method
            final ImmutableList<Parameter> params =
                    Invokable.from(abstractMethod.getMethod()).getParameters();
    
            for (Parameter param : params) {
                //if the param isn't marked as @Nullable,
                if (!param.isAnnotationPresent(Nullable.class)) {
                    //try getting the @QueryParam value
                    @Nullable final QueryParam queryParam =
                            param.getAnnotation(QueryParam.class);
                    //if it's present, add its value to the set
                    if (queryParam != null) {
                        requiredQueryParamsBuilder.add(queryParam.value());
                    }
                }
            }
    
            //return the new validation filter for this resource method
            return Collections.<ResourceFilter>singletonList(
                    new ValidationFilter(requiredQueryParamsBuilder.build())
            );
        }
    
        private static final class ValidationFilter implements ResourceFilter {
    
            final ImmutableSet<String> requiredQueryParams;
    
            private ValidationFilter(ImmutableSet<String> requiredQueryParams) {
                this.requiredQueryParams = requiredQueryParams;
            }
    
            @Override
            public ContainerRequestFilter getRequestFilter() {
                return new ContainerRequestFilter() {
                    @Override
                    public ContainerRequest filter(ContainerRequest request) {
    
                        final Collection<String> missingRequiredParams =
                                Sets.difference(
                                        requiredQueryParams,
                                        request.getQueryParameters().keySet()
                                );
    
                        if (!missingRequiredParams.isEmpty()) {
    
                            final String message =
                                    "Required query params missing: " +
                                    Joiner.on(", ").join(missingRequiredParams);
    
                            final Response response = Response
                                    .status(Status.BAD_REQUEST)
                                    .entity(message)
                                    .build();
    
                            throw new WebApplicationException(response);
                        }
    
                        return request;
                    }
                };
            }
    
            @Override
            public ContainerResponseFilter getResponseFilter() {
                return null;
            }
        }
    }
    

    And the ResourceFilterFactory is registered with Jersey as an init param of the servlet in web.xml:

    <init-param>
        <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
        <param-value>my.package.name.ValidationFilterFactory</param-value>
    </init-param>
    

    At startup, ValidationFilterFactory.create gets called for each resource method detected by Jersey.

    Credit goes to this post for getting me on the right track: How can I get resource annotations in a Jersey ContainerResponseFilter