Skip to content

Commit

Permalink
issue-42851: port resteasy fix for sub-resources from resteasy/restea…
Browse files Browse the repository at this point in the history
…sy-microprofile PR quarkusio#241
  • Loading branch information
maxr2011-tech11 committed Sep 4, 2024
1 parent 092aaf0 commit 6d505cd
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package io.quarkus.restclient.exception;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;

import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Unremovable;
import io.quarkus.test.QuarkusUnitTest;

/**
* Tests client sub-resources
*
* @author <a href="mailto:[email protected]">James R. Perkins</a>
*/
public class SubResourceTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(SubResourceTest.class, ClientRootResource.class, ClientSubResource.class,
ServerResource.class, TestExceptionMapper.class, TestException.class)
.addAsResource(new StringAsset(ClientRootResource.class.getName() + "/mp-rest/url=${test.url}\n"),
"application.properties"));

@Inject
@RestClient
ClientRootResource clientRootResource;

/**
* Creates a REST client with an attached exception mapper. The exception mapper will throw a {@link TestException}.
* This test invokes a call to the root resource.
*
*/
@Test
public void rootResourceExceptionMapper() {
try (final Response ignored = clientRootResource.fromRoot()) {
fail("fromRoot() should have thrown a TestException");
} catch (TestException expected) {
assertEquals("RootResource failed on purpose", expected.getMessage());
}
}

/**
* Creates a REST client with an attached exception mapper. The exception mapper will throw a {@link TestException}.
* This test invokes a call to the sub-resource. The sub-resource then invokes an additional call which should also
* result in a {@link TestException} thrown.
*
* @throws Exception if a test error occurs
*/
@Test
public void subResourceExceptionMapper() throws Exception {
try (final Response ignored = clientRootResource.subResource().fromSub()) {
fail("fromSub() should have thrown a TestException");
} catch (TestException expected) {
assertEquals("SubResource failed on purpose", expected.getMessage());
}
}

/**
* This test invokes a call to the sub-resource. The sub-resource then invokes an additional call which should
* return the header value for {@code test-header}.
*
*/
@Test
public void subResourceWithHeader() {
try (final Response response = clientRootResource.subResource().withHeader()) {
assertEquals(Response.Status.OK, response.getStatusInfo());
assertEquals("SubResourceHeader", response.readEntity(String.class));
}
}

/**
* This test invokes a call to the sub-resource. The sub-resource then invokes an additional call which should
* return the header value for {@code test-global-header}.
*
* @throws Exception if a test error occurs
*/
@Test
public void subResourceWithGlobalHeader() throws Exception {
try (final Response response = clientRootResource.subResource().withGlobalHeader()) {
assertEquals(Response.Status.OK, response.getStatusInfo());
assertEquals("GlobalSubResourceHeader", response.readEntity(String.class));
}
}

@RegisterRestClient
@RegisterProvider(TestExceptionMapper.class)
@Path("/root")
public interface ClientRootResource {
@Path("/sub")
ClientSubResource subResource();

@GET
Response fromRoot() throws TestException;
}

@ClientHeaderParam(name = "test-global-header", value = "GlobalSubResourceHeader")
@Produces(MediaType.TEXT_PLAIN)
public interface ClientSubResource {
@GET
Response fromSub() throws TestException;

@GET
@ClientHeaderParam(name = "test-header", value = "SubResourceHeader")
@Path("/header")
Response withHeader();

@GET
@Path("/global/header")
Response withGlobalHeader();
}

@Path("/root")
public static class ServerResource {
@GET
public Response fromRoot() {
return Response.serverError().entity("RootResource failed on purpose").build();
}

@GET
@Path("/sub")
public Response fromSub() {
return Response.serverError().entity("SubResource failed on purpose").build();
}

@GET
@Path("/sub/header")
@Produces(MediaType.TEXT_PLAIN)
public Response subHeader(@HeaderParam("test-header") final String value) {
return Response.ok(value).build();
}

@GET
@Path("/sub/global/header")
@Produces(MediaType.TEXT_PLAIN)
public Response subGlobalHeader(@HeaderParam("test-global-header") final String value) {
return Response.ok(value).build();
}
}

@Provider
@Unremovable
public static class TestExceptionMapper implements ResponseExceptionMapper<TestException> {
@Override
public TestException toThrowable(final Response response) {
return new TestException(response.readEntity(String.class));
}
}

public static class TestException extends RuntimeException {
public TestException(final String msg) {
super(msg);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.quarkus.restclient.runtime;

import java.io.Closeable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -22,6 +24,8 @@
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.InterceptionType;
import jakarta.enterprise.inject.spi.Interceptor;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ResponseProcessingException;
import jakarta.ws.rs.ext.ParamConverter;
Expand All @@ -30,8 +34,10 @@
import org.jboss.logging.Logger;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.microprofile.client.ExceptionMapping;
import org.jboss.resteasy.microprofile.client.ProxyInvocationHandler;
import org.jboss.resteasy.microprofile.client.RestClientProxy;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderProviders;

/**
* Quarkus version of {@link org.jboss.resteasy.microprofile.client.ProxyInvocationHandler} retaining the ability to
Expand Down Expand Up @@ -84,14 +90,17 @@ public QuarkusProxyInvocationHandler(final Class<?> restClientInterface,
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (RestClientProxy.class.equals(method.getDeclaringClass())) {
return invokeRestClientProxyMethod(proxy, method, args);
return invokeRestClientProxyMethod(method);
}
// Autocloseable/Closeable
if (method.getName().equals("close") && (args == null || args.length == 0)) {
close();
return null;
}
if (closed.get()) {
// Check if this proxy is closed or the client itself is closed. The client may be closed if this proxy was a
// sub-resource and the resource client itself was closed.
if (closed.get() || client.isClosed()) {
closed.set(true);
throw new IllegalStateException("RestClientProxy is closed");
}

Expand Down Expand Up @@ -162,7 +171,30 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
return new QuarkusInvocationContextImpl(target, method, args, chain, interceptorBindingsMap.get(method)).proceed();
} else {
try {
return method.invoke(target, args);
final Object result = method.invoke(target, args);
final Class<?> returnType = method.getReturnType();
// Check if this is a sub-resource. A sub-resource must be an interface.
if (returnType.isInterface()) {
final Annotation[] annotations = method.getDeclaredAnnotations();
boolean hasPath = false;
boolean hasHttpMethod = false;
// Check the annotations. If the method has one of the @HttpMethod annotations, we will just use the
// current method. If it only has a @Path, then we need to create a proxy for the return type.
for (Annotation annotation : annotations) {
final Class<?> type = annotation.annotationType();
if (type.equals(Path.class)) {
hasPath = true;
} else if (type.getDeclaredAnnotation(HttpMethod.class) != null) {
hasHttpMethod = true;
}
}
if (!hasHttpMethod && hasPath) {
// Create a proxy of the return type re-using the providers and client, but do not add the required
// interfaces for the sub-resource.
return createProxy(returnType, result, false, providerInstances, client, getBeanManager());
}
}
return result;
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof CompletionException) {
Expand Down Expand Up @@ -193,7 +225,56 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
}
}

private Object invokeRestClientProxyMethod(Object proxy, Method method, Object[] args) {
/**
* Creates a proxy for the interface. The proxy will implement the interfaces {@link RestClientProxy} and
* {@link Closeable}.
*
* @param resourceInterface the resource interface to create the proxy for
* @param target the target object for the proxy
* @param providers the providers for the client
* @param client the client to use
* @param beanManager the bean manager used to register {@linkplain ClientHeaderProviders client header providers}
* @return the new proxy
*/
static Object createProxy(final Class<?> resourceInterface, final Object target, final Set<Object> providers,
final ResteasyClient client, final BeanManager beanManager) {
return createProxy(resourceInterface, target, true, providers, client, beanManager);
}

/**
* Creates a proxy for the interface.
* <p>
* If {@code addExtendedInterfaces} is set to {@code true}, the proxy will implement the interfaces
* {@link RestClientProxy} and {@link Closeable}.
* </p>
*
* @param resourceInterface the resource interface to create the proxy for
* @param target the target object for the proxy
* @param addExtendedInterfaces {@code true} if the proxy should also implement {@link RestClientProxy} and
* {@link Closeable}
* @param providers the providers for the client
* @param client the client to use
* @param beanManager the bean manager used to register {@linkplain ClientHeaderProviders client header providers}
* @return the new proxy
*/
static Object createProxy(final Class<?> resourceInterface, final Object target, final boolean addExtendedInterfaces,
final Set<Object> providers, final ResteasyClient client, final BeanManager beanManager) {
final Class<?>[] interfaces;
if (addExtendedInterfaces) {
interfaces = new Class<?>[3];
interfaces[1] = RestClientProxy.class;
interfaces[2] = Closeable.class;
} else {
interfaces = new Class[1];
}
interfaces[0] = resourceInterface;
final Object proxy = Proxy.newProxyInstance(resourceInterface.getClassLoader(), interfaces,
new ProxyInvocationHandler(resourceInterface, target, Set.copyOf(providers), client));
ClientHeaderProviders.registerForClass(resourceInterface, proxy, beanManager);
return proxy;
}

private Object invokeRestClientProxyMethod(final Method method) {
switch (method.getName()) {
case "getClient":
return client;
Expand Down Expand Up @@ -299,4 +380,13 @@ private static Annotation[] merge(List<Annotation> methodLevelBindings, List<Ann
return merged.toArray(new Annotation[] {});
}

private static BeanManager getBeanManager() {
try {
CDI<Object> current = CDI.current();
return current != null ? current.getBeanManager() : null;
} catch (IllegalStateException e) {
LOGGER.debug("CDI container is not available", e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@
import static org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder.PROPERTY_PROXY_PORT;
import static org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder.PROPERTY_PROXY_SCHEME;

import java.io.Closeable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
Expand Down Expand Up @@ -72,10 +70,8 @@
import org.jboss.resteasy.microprofile.client.ExceptionMapping;
import org.jboss.resteasy.microprofile.client.MethodInjectionFilter;
import org.jboss.resteasy.microprofile.client.RestClientListeners;
import org.jboss.resteasy.microprofile.client.RestClientProxy;
import org.jboss.resteasy.microprofile.client.async.AsyncInterceptorRxInvokerProvider;
import org.jboss.resteasy.microprofile.client.async.AsyncInvocationInterceptorThreadContext;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderProviders;
import org.jboss.resteasy.microprofile.client.header.ClientHeadersRequestFilter;
import org.jboss.resteasy.microprofile.client.impl.MpClient;
import org.jboss.resteasy.microprofile.client.impl.MpClientBuilderImpl;
Expand Down Expand Up @@ -353,16 +349,10 @@ public <T> T build(Class<T> aClass, ClientHttpEngine httpEngine)
.defaultConsumes(MediaType.APPLICATION_JSON)
.defaultProduces(MediaType.APPLICATION_JSON).build();

Class<?>[] interfaces = new Class<?>[3];
interfaces[0] = aClass;
interfaces[1] = RestClientProxy.class;
interfaces[2] = Closeable.class;

final BeanManager beanManager = getBeanManager();
T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces,
new QuarkusProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client, beanManager));
ClientHeaderProviders.registerForClass(aClass, proxy, beanManager);
return proxy;
return aClass.cast(
QuarkusProxyInvocationHandler.createProxy(aClass, actualClient, getLocalProviderInstances(), client,
beanManager));
}

private void configureTrustAll(ResteasyClientBuilder clientBuilder) {
Expand Down

0 comments on commit 6d505cd

Please sign in to comment.