Monday, July 25, 2016

Conditional JAX-RS filter

JAX-RS has the notion of a DynamicFeature: this is a feature that is decided on a per-resource-method basis. This means that we can fine tune the behavior of the application based on each REST endpoint/method. Imagine the following use case: you have a multi tenant system and you want to create a filter that blocks incoming requests if they don't provide the tenant information. By default, all endpoints require a tenant. But there are some endpoints or methods that don't require this tenant information. Instead of building a generic filter that will be processed for all incoming requests, you can just create an annotation and mark the places you don't want the filter to run.
First, your DynamicFeature needs to implement the configure(ResourceInfo, FeatureContext) method, like this:
@Provider
public class TenantFeature implements DynamicFeature {
    private static final TenantFilter TENANT_FILTER = new TenantFilter();

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

        boolean required = true;
        if (resourceClass.isAnnotationPresent(TenantRequired.class)) {
            required = resourceClass.getAnnotation(TenantRequired.class).value();
        }

        if (method.isAnnotationPresent(TenantRequired.class)) {
            required = method.getAnnotation(TenantRequired.class).value();
        }

        if (required) {
            context.register(TENANT_FILTER);
        }
    }
}
Then our actual filter. Note that this is not marked with @Provider, as we don't want this applied to all resources discovered by the JAX-RS implementation.
public class TenantFilter implements ContainerRequestFilter {
    private static final String TENANT_HEADER_NAME = "Tenant";
    private static final String MESSAGE = String.format("The HTTP header %s has to be provided.", TENANT_HEADER_NAME);
    private static final Response BAD_REQUEST_MISSING_TENANT = Response
            .status(Response.Status.BAD_REQUEST)
            .entity(MESSAGE)
            .build();

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String headerValue = requestContext.getHeaderString(TENANT_HEADER_NAME);
        if (null == headerValue || headerValue.isEmpty()) {
            requestContext.abortWith(BAD_REQUEST_MISSING_TENANT);
        }
    }
And finally, the annotation we'll use to mark the parts we don't want the filter to run on. Note that the default value is set to true.
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface TenantRequired {
    @Nonbinding
    boolean value() default true;
}
Our business code would look like this. For all non-annotated methods/classes, the filter runs on. So, GET /foo requires our tenant header, whereas a POST /foo doesn't.
@Path("/foo")
public class FooEndpoint {
    @GET
    public Response getFoo() {
        return Response.ok("bar").build();
    }

    @TenantRequired(false)
    @POST
    public Response getFooWithoutTenant() {
        return Response.ok("bar").build();
    }
}
Or we could mark the entire endpoint as not requiring a tenant:
@Path("no-tenant")
@TenantRequired(false)
public class FooNoTenantEndpoint {

    @GET
    public Response getFoo() {
        return Response.ok("bar").build();
    }
}
Resource Link: https://blog.kroehling.de/conditional-jax-rs-filter/

No comments:

Post a Comment