1

Is it possible to register a DynamicFeature with an ResteasyClient (Proxy Framework) similar to what can be done on server side?

So something similar to this:

final ResteasyClient client = new ResteasyClientBuilder().build();
client.register(new MyDynamicFeature());

Where MyDynamicFeature implements DynamicFeature

I'm trying to figure out how to have a ClientResponseFilter check the http return status depending on the annotation that is present on the resource method, and the DynamicFeature appeared to be the most promising lead to get access to the ResourceInfo.

So essentially, I want to do something like this:

@POST
@Path("some/path/user")
@ExpectedHttpStatus(201) // <- this would have to be passed on somehow as expectedStatus
User createUser(User request);

And then in the ClientResponseFilter (or any other solution) something like this:

@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
    if (responseContext.getStatus() != expectedStatus) {
        // explode
    }
}

Cause in the ClientResponseFilter, I don't see any way to know what the resource method is that defined the REST call that the filter is currently analyzing.

And the problem is that the framework right now only checks whether the response status is success, it doesn't check whether it's 200 or 201 and we'd like to refine that.

Here are some articles that seems to explain something very similar, yet this doesn't seem to be working with the ClientResponseFilter / ResteasyClient:

Community
  • 1
  • 1
mac
  • 2,672
  • 4
  • 31
  • 43

2 Answers2

1

First of all, I can't take credit for the solution really, but I'm going to paste the answer here.

Also, you could ask why the heck we're doing this? Because we need / want to test that the service returns the right http status, but unfortunately the service we are testing does not always return the same http status for the same http method.

E.g. in the example below, the post returns HttpStatus.OK, and another post method of the same service could return HttpStatus.CREATED.

Here's the solution we ended up with, a combination of ClientResponseFilter:

import java.io.IOException;
import java.util.UUID;

import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;

/**
 * {@link ClientResponseFilter} which will handle setting the HTTP StatusCode property for use with
 * {@link HttpStatusResponseInterceptor}
 */
public class HttpStatusResponseFilter implements ClientResponseFilter {

    public static final String STATUS_CODE = "StatusCode-" + UUID.randomUUID();

    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
        requestContext.setProperty(STATUS_CODE, responseContext.getStatusInfo());
    }
}

And ReaderInterceptor:

import java.io.IOException;
import java.lang.annotation.Annotation;

import javax.ws.rs.ServerErrorException;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;

/**
 * {@link ReaderInterceptor} which will verify the success HTTP status code returned from the server against the
 * expected successful HTTP status code {@link SuccessStatus}
 *
 * @see HttpStatusResponseFilter
 */
public class HttpStatusResponseInterceptor implements ReaderInterceptor {

    @Override
    public Object aroundReadFrom(ReaderInterceptorContext interceptorContext) throws ServerErrorException, IOException {
        Status actualStatus = (Status) interceptorContext.getProperty(HttpStatusResponseFilter.STATUS_CODE);
        if (actualStatus == null) {
            throw new IllegalStateException("Property " + HttpStatusResponseFilter.STATUS_CODE + " does not exist!");
        }

        Status expectedStatus = null;
        for (Annotation annotation : interceptorContext.getAnnotations()) {
            if (annotation.annotationType() == SuccessStatus.class) {
                expectedStatus = ((SuccessStatus) annotation).value();
                break;
            }
        }

        if (expectedStatus != null && expectedStatus != actualStatus) {
            throw new ServerErrorException(String.format("Invalid status code returned. Expected %d, but got %d.",
                    expectedStatus.getStatusCode(), actualStatus.getStatusCode()), actualStatus);
        }

        return interceptorContext.proceed();
    }
}

We register both those when we create the client:

    final ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
    client.register(new HttpStatusResponseFilter());
    client.register(new HttpStatusResponseInterceptor());

And the SuccessStatus is an annotation that we use to annotate the methods that we want to specifically check, e.g. like that:

@POST
@Path("some/foobar")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@SuccessStatus(Status.OK)
Foobar createFoobar(Foobar foobar);
mac
  • 2,672
  • 4
  • 31
  • 43
0

It's not possible to register a DynamicFeature in your client.

See the DynamicFeature documentation:

A JAX-RS meta-provider for dynamic registration of post-matching providers during a JAX-RS application setup at deployment time. Dynamic feature is used by JAX-RS runtime to register providers that shall be applied to a particular resource class and method and overrides any annotation-based binding definitions defined on any registered resource filter or interceptor instance.

Providers implementing this interface MAY be annotated with @Provider annotation in order to be discovered by JAX-RS runtime when scanning for resources and providers. This provider types is supported only as part of the Server API.

The JAX-RS Client API can be utilized to consume any Web service exposed on top of a HTTP protocol, and is not restricted to services implemented using JAX-RS.

Please note the JAX-RS Client API does not invoke the resource classes directly. Instead, it generates HTTP requests to the server. Consequently, you won't be able to read the annotations from your resource classes.


Update 1

I'm not sure if this will be useful for you, but since you would like to access the server resource classes from your client, it would be interesting to mention that Jersey provides a proxy-based client API (org.glassfish.jersey.client.proxy package).

The basic idea is you can attach the standard JAX-RS annotations to an interface, and then implement that interface by a resource class on the server side while reusing the same interface on the client side by dynamically generating an implementation of that using java.lang.reflect.Proxy calling the right low-level client API methods.

This example was extracted from Jersey documentation:

Consider a server which exposes a resource at http://localhost:8080. The resource can be described by the following interface:

@Path("myresource")
public interface MyResourceIfc {

    @GET
    @Produces("text/plain")
    String get();

    @POST
    @Consumes("application/xml")
    @Produces("application/xml")
    MyBean postEcho(MyBean bean);

    @GET
    @Path("{id}")
    @Produces("text/plain")
    String getById(@PathParam("id") String id);
}

You can use WebResourceFactory class defined in this package to access the server-side resource using this interface. Here is an example:

Client client = ClientBuilder.newClient();
WebTarget target = client.target("http://localhost:8080/");
MyResourceIfc resource = WebResourceFactory.newResource(MyResourceIfc.class, target);

String responseFromGet = resource.get();
MyBean responseFromPost = resource.postEcho(myBeanInstance);
String responseFromGetById = resource.getById("abc");

I'm not sure if RESTEasy provides something similar to it.


Update 2

RESTEasy also provides a proxy framework. See the documentation:

RESTEasy has a client proxy framework that allows you to use JAX-RS annotations to invoke on a remote HTTP resource. The way it works is that you write a Java interface and use JAX-RS annotations on methods and the interface. For example:

public interface SimpleClient {

    @GET
    @Path("basic")
    @Produces("text/plain")
    String getBasic();

    @PUT
    @Path("basic")
    @Consumes("text/plain")
    void putBasic(String body);

    @GET
    @Path("queryParam")
    @Produces("text/plain")
    String getQueryParam(@QueryParam("param") String param);

    @GET
    @Path("matrixParam")
    @Produces("text/plain")
    String getMatrixParam(@MatrixParam("param") String param);

    @GET
    @Path("uriParam/{param}")
    @Produces("text/plain")
    int getUriParam(@PathParam("param") int param);
}

RESTEasy has a simple API based on Apache HttpClient. You generate a proxy then you can invoke methods on the proxy. The invoked method gets translated to an HTTP request based on how you annotated the method and posted to the server. Here's how you would set this up:

Client client = ClientFactory.newClient();
WebTarget target = client.target("http://example.com/base/uri");
ResteasyWebTarget rtarget = (ResteasyWebTarget) target;

SimpleClient simple = rtarget.proxy(SimpleClient.class);
simple.putBasic("hello world");

Alternatively you can use the RESTEasy client extension interfaces directly:

ResteasyClient client = new ResteasyClientBuilder().build();
ResteasyWebTarget target = client.target("http://example.com/base/uri");

SimpleClient simple = target.proxy(SimpleClient.class);
simple.putBasic("hello world");

[...]

The framework also supports the JAX-RS locator pattern, but on the client side. So, if you have a method annotated only with @Path, that proxy method will return a new proxy of the interface returned by that method.

[...]

It is generally possible to share an interface between the client and server. In this scenario, you just have your JAX-RS services implement an annotated interface and then reuse that same interface to create client proxies to invoke on the client-side.


Update 3

Since you are already using RESTEasy Proxy Framework and assuming your server resources implement the same interfaces you are using to create your client proxies, the following solution should work.

A ProxyFactory from Spring AOP, which is already packed with RESTEasy Client will do trick. This solution, basically, creates a proxy of the proxy to intercept the method that is being invoked.

The following class stores the Method instance:

public class MethodWrapper {

    private Method method;

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }
}

And the following code makes the magic:

ResteasyClient client = new ResteasyClientBuilder().build();
ResteasyWebTarget target = client.target("http://example.com/api");
ExampleResource resource = target.proxy(ExampleResource.class);

MethodWrapper wrapper = new MethodWrapper();

ProxyFactory proxyFactory = new ProxyFactory(resource);
proxyFactory.addAdvice(new MethodInterceptor() {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        wrapper.setMethod(invocation.getMethod());
        return invocation.proceed();
    }
});

ExampleResource resourceProxy = (ExampleResource) proxyFactory.getProxy();
Response response = resourceProxy.doSomething("Hello World!");

Method method = wrapper.getMethod();
ExpectedHttpStatus expectedHttpStatus = method.getAnnotation(ExpectedHttpStatus.class);

int status = response.getStatus();
int expectedStatus = annotation.status();

For more information, have a look at the documentation:

cassiomolin
  • 124,154
  • 35
  • 280
  • 359
  • Yes, so we're using the RESTeasy proxy framework. And so the question is now, how do I squeeze in a response filter that knows which resource is currently being processed so that I can add a custom annotation to methods defined in the interface and pass information via its values to the response filter. I will add clarification above. – mac Nov 25 '15 at 16:17
  • 1
    sorry ... crazy times right now. We actually went down another path. I'll post something once I have more breathing room. – mac Dec 09 '15 at 17:17
  • I have posted our solution now. Regarding your update #3: Not sure how that would work to be honest for different methods. We're not creating the client for every method we execute. So not sure how we'd get access to that information in our scenario with your solution. – mac Feb 07 '16 at 06:29