Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jersey/Jax-RS: how to filter resource and sub-resources

In Jersey 2, how can I bind a filter to all the method of a Resource as well as to all the methods of its sub-resources?

For example, if I have the following 2 resources:

import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.server.model.Resource;

@Path("/myresource/{id: \\d+}")
@Produces(MediaType.APPLICATION_JSON)
@Singleton
class RootResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response get(@PathParam("id") Long id) {
        return Response.ok().build();
    }

    @Path("/sub")
    public Resource getSubResource() {
        return Resource.from(SubResource.class);
    }
}

@Produces(MediaType.APPLICATION_JSON)
@Singleton
class SubResource {
    @GET
    @Path("/{subid: \\d+}")
    public Response get(@PathParam("id") Long id, @PathParam("subid") Long subid) {
        return Response.ok().build();
    }
}

I would like to filter RootResource.get(Long) and SubResource.get(Long, Long). But if I have other resources, those should not be filtered.

Using the DynamicFeature, we only have info on the Class and Method.

import javax.ws.rs.container.DynamicFeature;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.FeatureContext;

public class MyFeature implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        // Here how can I find out that SubResource is actually a sub-resource of RootResource
    }

}

The idea is that I want to be able to filter out all calls for a certain set of id's (the set of id's is dynamic), with something more or less like this:

import java.io.IOException;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;


public class MyFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        for(Object resource:requestContext.getUriInfo().getMatchedResources()) {
            if(resource instanceof RootResource) {
                Long id = Long.valueOf(requestContext.getUriInfo().getPathParameters().getFirst("id"));
                // ...
            }
        }
    }

}

but I would like to avoid having to search for the matched resources. Is this possible?

like image 862
Guillaume Polet Avatar asked Apr 02 '15 07:04

Guillaume Polet


People also ask

Is Jax and Jersey RS the same?

JAX-RS is an specification (just a definition) and Jersey is a JAX-RS implementation. Jersey framework is more than the JAX-RS Reference Implementation. Jersey provides its own API that extend the JAX-RS toolkit with additional features and utilities to further simplify RESTful service and client development.

What is JAX-RS and Jersey?

The JAX-RS (JSR 311: The Java API for RESTful Web Services) specification provides a standardized Java-based approach to implementing REST-style web services. Jersey is the reference implementation of JAX-RS and I provide a brief introduction to JAX-RS via Jersey in this blog post.

What is a JAX-RS resource?

JAX-RS is a Java programming language API designed to make it easy to develop applications that use the REST architecture. The JAX-RS API uses Java programming language annotations to simplify the development of RESTful web services.

What can a JAX-RS method return?

If the URI path template variable cannot be cast to the specified type, the JAX-RS runtime returns an HTTP 400 (“Bad Request”) error to the client. If the @PathParam annotation cannot be cast to the specified type, the JAX-RS runtime returns an HTTP 404 (“Not Found”) error to the client.


2 Answers

I'm not 100% sure I understand the problem, but it seems that you want to want to limit which resources should go through the filter. For that you can simply use Name Binding.

Basic Steps:

  1. Create a @NameBinding annotation

    @NameBinding 
    @Target({METHOD, TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Filtered {    
    }
    
  2. Annotate the filter

    @Filtered
    @Provider
    public class MyFilter implements ContainerRequestFilter {
    
  3. Annotate whatever root resources, resource methods, sub resource classes you want to be filtered


UPDATE

OK so after some playing around, I came up with a couple solutions.. netiher of which are pretty, but it gets the job done.

Keep in mind that configure in the DynamicFeature is called for each resource(method) we have.

Algorithm 1:

  1. Get the method being checked and get its declaring class (in the case of a method in a sub resource, the declaring class will be the sub resource class)

    Class<?> possibleSubResource =
             resourceInfo.getResourceMethod().getDeclaringClass();
    
  2. Build a temporary Resource from your root resource

    Resource resource = Resource.from(SomeResource.class);
    
  3. Iterate its child resources, checking if it's a resource locator

    for (Resource childResource : resource.getChildResources()) {
        if (childResource.getResourceLocator() != null) {
    
  4. If is is resource locator get the return type.

    ResourceMethod sub = childResource.getResourceLocator();
    Class responseClass = sub.getInvocable().getRawResponseType();
    
  5. Then check if the response type from step 4 == the declaring class from step 1.

    if (responseClass == possibleSubResource) {
        context.register(SomeFilter.class);
    }
    

For the above to work, you actually need to return the sub resource type from the locator method, instead of a Resource. (You can try and make it work with Resource, but I haven't been able to figure it out)

@Path("{id}")
public SomeSubResource getSubResource() {
    return new SomeSubResource();
}

Here is the full code that works (not battle tested :-)

@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
    Class<?> resourceClass = resourceInfo.getResourceClass();

    if (resourceClass == SomeResource.class) {
        context.register(SomeFilter.class);
    }

    Class<?> possibleSubResource = resourceInfo.getResourceMethod().getDeclaringClass();

    Resource resource = Resource.from(SomeResource.class);
    for (Resource childResource : resource.getChildResources()) {
        if (childResource.getResourceLocator() != null) {
            ResourceMethod sub = childResource.getResourceLocator();
            Class responseClass = sub.getInvocable().getRawResponseType();

            if (responseClass == possibleSubResource) {
                context.register(SomeFilter.class);
            }
        }
    }
}

Algorithm 2:

For this to work, we are going based off the assumption that what defines a Sub Resource is that it is annotation with @Path and has no Http method annotation

  1. Get the method being checked and get its declaring class (in the case of a method in a sub resource, the declaring class will be the sub resource class)

    Class<?> possibleSubResource =
             resourceInfo.getResourceMethod().getDeclaringClass();
    
  2. Iterate through the Methods in the root resource class

    for (Method method : SomeResource.class.getDeclaredMethods()) {
    
  3. Check if the method has an Http method annotation

    boolean isHttpPresent = false;
    for (Class annot : Arrays.asList(GET.class, POST.class, PUT.class, DELETE.class)) {
        if (method.isAnnotationPresent(annot)) {
            isHttpPresent = true;
            break;
        }
    }
    
  4. Check if the method has the @Path annotation. If it does, and it has not Http method annotations, then we register the filter

    if (method.isAnnotationPresent(Path.class) && !isHttpPresent) {
        Class subResourceClass = method.getReturnType();
        if (subResourceClass == possibleSubResource) {
            context.register(SomeFilter.class);
        }
    }
    

Here is the full code

@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
    Class<?> resourceClass = resourceInfo.getResourceClass();

    if (resourceClass == SomeResource.class) {
        context.register(SomeFilter.class);
    }

    Class<?> possibleSubResource = resourceInfo.getResourceMethod().getDeclaringClass();

    for (Method method : SomeResource.class.getDeclaredMethods()) {
        boolean isHttpPresent = false;
        for(Class annot : Arrays.asList(GET.class,POST.class,PUT.class, DELETE.class)){
            if (method.isAnnotationPresent(annot)) {
                isHttpPresent = true;
                break;
            }
        }
        if(method.isAnnotationPresent(Path.class) && !isHttpPresent){
            Class subResourceClass = method.getReturnType();
            if (subResourceClass == possibleSubResource) {
                context.register(SomeFilter.class);
            }
        }
    }
}

Again, neither of these solutions are battled tested, but work for the few cases I have tried. Personally, I'd just go with the name binding, but maybe this is an issue you can raise with the Jersey team. This (automatic registration of sub resource, when the root resources are registered) does seem like something that should work out the box, or at least be able to be configured.

like image 54
Paul Samsotha Avatar answered Oct 22 '22 11:10

Paul Samsotha


I had a similar need: I wanted an annotation to specifically filter resource methods in order to achieve something like this:

@Path("/api/sample")
@Produces(MediaType.APPLICATION_JSON)
public class SampleResource {

    @Path("/filtered")
    @GET
    @Sample(value = "a sample value")
    public Hello filtered() {
        return new Hello("filtered hello");
    }

    @Path("/nonfiltered")
    @GET
    public Hello raw() {
        return new Hello("raw hello");
    }
}

My annotation being:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sample {

    String value() default "";
}

I ended up using a DynamicFeature to register a Filter on Resource

@Provider
public class SampleFeature implements DynamicFeature {

    private SampleFilter sampleFilter;

    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        if (resourceInfo.getResourceMethod().getAnnotation(Sample.class) != null) {
            if (sampleFilter == null) {
                this.sampleFilter = new SampleFilter();
            }
            context.register(sampleFilter);
        }
    }
}

the tricky thing was to find out how I could fetch the annotation value within my filter, hence to find out about ExtendedUriInfo, see below:

public class SampleFilter implements ContainerRequestFilter {

    public SampleFilter() {
    }

    public void filter(ContainerRequestContext containerRequestContext) throws IOException {
        String sampleValue = this.getAnnotation(containerRequestContext).value();
        // do some filtering based on the Sample Value
        }

    private Sample getAnnotation(ContainerRequestContext requestContext) {
        ResourceMethod method = ((ExtendedUriInfo) (requestContext.getUriInfo()))
                .getMatchedResourceMethod();
        Method invokedMethod = method.getInvocable().getHandlingMethod();
        return invokedMethod.getAnnotation(Sample.class);
    }
}
like image 2
Francois Avatar answered Oct 22 '22 11:10

Francois