From 4628ea3d38ab9152ef053ddc51b233cc4229b2ef Mon Sep 17 00:00:00 2001 From: Maximilian Rehberger Date: Fri, 30 Aug 2024 14:11:16 +0200 Subject: [PATCH] issue-42851: port resteasy fix for sub-resources from resteasy/resteasy-microprofile PR #241 --- .../QuarkusProxyInvocationHandler.java | 133 +++++++++++++++--- .../runtime/QuarkusRestClientBuilder.java | 12 +- 2 files changed, 114 insertions(+), 31 deletions(-) diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java index 022f4b53305b3a..b58f40c9134ab0 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java @@ -1,37 +1,33 @@ package io.quarkus.restclient.runtime; -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.Type; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - import jakarta.enterprise.context.spi.CreationalContext; import jakarta.enterprise.inject.spi.BeanManager; 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; import jakarta.ws.rs.ext.ParamConverterProvider; - 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; + +import java.io.Closeable; +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; /** * Quarkus version of {@link org.jboss.resteasy.microprofile.client.ProxyInvocationHandler} retaining the ability to @@ -84,14 +80,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"); } @@ -162,7 +161,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) { @@ -193,7 +215,57 @@ 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 providers, + final ResteasyClient client, final BeanManager beanManager) { + return createProxy(resourceInterface, target, true, providers, client, beanManager); + } + + /** + * Creates a proxy for the interface. + *

+ * If {@code addExtendedInterfaces} is set to {@code true}, 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 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 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(getClassLoader(resourceInterface), 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; @@ -299,4 +371,21 @@ private static Annotation[] merge(List methodLevelBindings, List type) { + if (System.getSecurityManager() == null) { + return type.getClassLoader(); + } + return AccessController.doPrivileged((PrivilegedAction) type::getClassLoader); + } + + private static BeanManager getBeanManager() { + try { + CDI current = CDI.current(); + return current != null ? current.getBeanManager() : null; + } catch (IllegalStateException e) { + LOGGER.debug("CDI container is not available", e); + return null; + } + } + } diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java index 7ecc462c52d120..19915310f848d7 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java @@ -353,16 +353,10 @@ public T build(Class 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) {