From e653e8f20d81973c895762e06798f106761694b1 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Mon, 25 Sep 2023 09:11:20 +0200 Subject: [PATCH] Make rest-client invocation context implement ArcInvocationContext --- .../runtime/QuarkusInvocationContextImpl.java | 198 ++++++++++++++++++ .../QuarkusProxyInvocationHandler.java | 26 ++- 2 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java new file mode 100644 index 0000000000000..6dc5856db69bf --- /dev/null +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java @@ -0,0 +1,198 @@ +package io.quarkus.restclient.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +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 jakarta.enterprise.inject.spi.InterceptionType; +import jakarta.enterprise.inject.spi.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.ws.rs.client.ResponseProcessingException; + +import org.jboss.resteasy.microprofile.client.ExceptionMapping; + +import io.quarkus.arc.ArcInvocationContext; + +/** + * A Quarkus copy of {@link org.jboss.resteasy.microprofile.client.InvocationContextImpl} which makes it implement + * {@link ArcInvocationContext} instead so that it's compatible with Quarkus interceptors. + */ +public class QuarkusInvocationContextImpl implements ArcInvocationContext { + + private final Object target; + + private final Method method; + + private Object[] args; + + private final int position; + + private final Map contextData; + + private final List chain; + + private final Set interceptorBindings; + + public QuarkusInvocationContextImpl(final Object target, final Method method, final Object[] args, + final List chain, Set interceptorBindings) { + this(target, method, args, chain, 0, interceptorBindings); + } + + private QuarkusInvocationContextImpl(final Object target, final Method method, final Object[] args, + final List chain, final int position, + Set interceptorBindings) { + this.target = target; + this.method = method; + this.args = args; + this.interceptorBindings = interceptorBindings == null ? Collections.emptySet() : interceptorBindings; + this.contextData = new HashMap<>(); + // put in bindings under Arc's specific key + this.contextData.put(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS, interceptorBindings); + this.position = position; + this.chain = chain; + } + + boolean hasNextInterceptor() { + return position < chain.size(); + } + + protected Object invokeNext() throws Exception { + return chain.get(position).invoke(nextContext()); + } + + private InvocationContext nextContext() { + return new QuarkusInvocationContextImpl(target, method, args, chain, position + 1, interceptorBindings); + } + + protected Object interceptorChainCompleted() throws Exception { + try { + return method.invoke(target, args); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof CompletionException) { + cause = cause.getCause(); + } + if (cause instanceof ExceptionMapping.HandlerException) { + ((ExceptionMapping.HandlerException) cause).mapException(method); + } + if (cause instanceof ResponseProcessingException) { + ResponseProcessingException rpe = (ResponseProcessingException) cause; + // Note that the default client engine leverages a single connection + // MP FT: we need to close the response otherwise we would not be able to retry if the method returns jakarta.ws.rs.core.Response + rpe.getResponse().close(); + cause = rpe.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + } + throw e; + } + } + + @Override + public Object proceed() throws Exception { + try { + if (hasNextInterceptor()) { + return invokeNext(); + } else { + return interceptorChainCompleted(); + } + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new RuntimeException(cause); + } + } + + @Override + public Object getTarget() { + return target; + } + + @Override + public Method getMethod() { + return method; + } + + @Override + public Constructor getConstructor() { + return null; + } + + @Override + public Object[] getParameters() throws IllegalStateException { + return args; + } + + @Override + public void setParameters(Object[] params) throws IllegalStateException, IllegalArgumentException { + this.args = params; + } + + @Override + public Map getContextData() { + return contextData; + } + + @Override + public Object getTimer() { + return null; + } + + @Override + public Set getInterceptorBindings() { + return interceptorBindings; + } + + @Override + public T findIterceptorBinding(Class annotationType) { + for (Annotation annotation : getInterceptorBindings()) { + if (annotation.annotationType().equals(annotationType)) { + return (T) annotation; + } + } + return null; + } + + @Override + public List findIterceptorBindings(Class annotationType) { + List found = new ArrayList<>(); + for (Annotation annotation : getInterceptorBindings()) { + if (annotation.annotationType().equals(annotationType)) { + found.add((T) annotation); + } + } + return found; + } + + public static class InterceptorInvocation { + + @SuppressWarnings("rawtypes") + private final Interceptor interceptor; + + private final Object interceptorInstance; + + public InterceptorInvocation(final Interceptor interceptor, final Object interceptorInstance) { + this.interceptor = interceptor; + this.interceptorInstance = interceptorInstance; + } + + @SuppressWarnings("unchecked") + Object invoke(InvocationContext ctx) throws Exception { + return interceptor.intercept(InterceptionType.AROUND_INVOKE, interceptorInstance, ctx); + } + } +} diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java index 2eba8ce96542c..022f4b53305b3 100644 --- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java @@ -30,7 +30,6 @@ 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.InvocationContextImpl; import org.jboss.resteasy.microprofile.client.RestClientProxy; import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException; @@ -52,7 +51,9 @@ public class QuarkusProxyInvocationHandler implements InvocationHandler { private final Set providerInstances; - private final Map> interceptorChains; + private final Map> interceptorChains; + + private final Map> interceptorBindingsMap; private final ResteasyClient client; @@ -70,10 +71,13 @@ public QuarkusProxyInvocationHandler(final Class restClientInterface, this.closed = new AtomicBoolean(); if (beanManager != null) { this.creationalContext = beanManager.createCreationalContext(null); - this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface); + this.interceptorBindingsMap = new HashMap<>(); + this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface, + interceptorBindingsMap); } else { this.creationalContext = null; this.interceptorChains = Collections.emptyMap(); + this.interceptorBindingsMap = Collections.emptyMap(); } } @@ -152,10 +156,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl args = argsReplacement; } - List chain = interceptorChains.get(method); + List chain = interceptorChains.get(method); if (chain != null) { // Invoke business method interceptors - return new InvocationContextImpl(target, method, args, chain).proceed(); + return new QuarkusInvocationContextImpl(target, method, args, chain, interceptorBindingsMap.get(method)).proceed(); } else { try { return method.invoke(target, args); @@ -245,10 +249,11 @@ private static BeanManager getBeanManager(Class restClientInterface) { } } - private static Map> initInterceptorChains( - BeanManager beanManager, CreationalContext creationalContext, Class restClientInterface) { + private static Map> initInterceptorChains( + BeanManager beanManager, CreationalContext creationalContext, Class restClientInterface, + Map> interceptorBindingsMap) { - Map> chains = new HashMap<>(); + Map> chains = new HashMap<>(); // Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases Map, Object> interceptorInstances = new HashMap<>(); @@ -267,12 +272,13 @@ private static Map> in List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE, interceptorBindings); if (!interceptors.isEmpty()) { - List chain = new ArrayList<>(); + List chain = new ArrayList<>(); for (Interceptor interceptor : interceptors) { - chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor, + chain.add(new QuarkusInvocationContextImpl.InterceptorInvocation(interceptor, interceptorInstances.computeIfAbsent(interceptor, i -> beanManager.getReference(i, i.getBeanClass(), creationalContext)))); } + interceptorBindingsMap.put(method, Set.of(interceptorBindings)); chains.put(method, chain); } }