Skip to content

Commit

Permalink
Introduce QuarkusProxyInvocationHandler to retain previous functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
manovotn committed Sep 8, 2023
1 parent 8c7ae7a commit 94791ab
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
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.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.InvocationContextImpl;
import org.jboss.resteasy.microprofile.client.RestClientProxy;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException;

/**
* Quarkus version of {@link org.jboss.resteasy.microprofile.client.ProxyInvocationHandler} retaining the ability to
* create a custom interceptor chain and invoke it manually.
* <p/>
* This is needed due to changes in https://github.com/resteasy/resteasy-microprofile/pull/182
* <p/>
* In theory, it could be improved by pre-generating proxies for {@code @RegisterRestClient} interfaces and registering
* them as standard beans with all their interceptor bindings.
*/
public class QuarkusProxyInvocationHandler implements InvocationHandler {

private static final Logger LOGGER = Logger.getLogger(QuarkusProxyInvocationHandler.class);
public static final Type[] NO_TYPES = {};

private final Object target;

private final Set<Object> providerInstances;

private final Map<Method, List<InvocationContextImpl.InterceptorInvocation>> interceptorChains;

private final ResteasyClient client;

private final CreationalContext<?> creationalContext;

private final AtomicBoolean closed;

public QuarkusProxyInvocationHandler(final Class<?> restClientInterface,
final Object target,
final Set<Object> providerInstances,
final ResteasyClient client, final BeanManager beanManager) {
this.target = target;
this.providerInstances = providerInstances;
this.client = client;
this.closed = new AtomicBoolean();
if (beanManager != null) {
this.creationalContext = beanManager.createCreationalContext(null);
this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface);
} else {
this.creationalContext = null;
this.interceptorChains = Collections.emptyMap();
}
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (RestClientProxy.class.equals(method.getDeclaringClass())) {
return invokeRestClientProxyMethod(proxy, method, args);
}
// Autocloseable/Closeable
if (method.getName().equals("close") && (args == null || args.length == 0)) {
close();
return null;
}
if (closed.get()) {
throw new IllegalStateException("RestClientProxy is closed");
}

boolean replacementNeeded = false;
Object[] argsReplacement = args != null ? new Object[args.length] : null;
Annotation[][] parameterAnnotations = method.getParameterAnnotations();

if (args != null) {
for (Object p : providerInstances) {
if (p instanceof ParamConverterProvider) {

int index = 0;
for (Object arg : args) {
// ParamConverter's are not allowed to be passed null values. If we have a null value do not process
// it through the provider.
if (arg == null) {
continue;
}

if (parameterAnnotations[index].length > 0) { // does a parameter converter apply?
ParamConverter<?> converter = ((ParamConverterProvider) p).getConverter(arg.getClass(), null,
parameterAnnotations[index]);
if (converter != null) {
Type[] genericTypes = getGenericTypes(converter.getClass());
if (genericTypes.length == 1) {

// minimum supported types
switch (genericTypes[0].getTypeName()) {
case "java.lang.String":
@SuppressWarnings("unchecked")
ParamConverter<String> stringConverter = (ParamConverter<String>) converter;
argsReplacement[index] = stringConverter.toString((String) arg);
replacementNeeded = true;
break;
case "java.lang.Integer":
@SuppressWarnings("unchecked")
ParamConverter<Integer> intConverter = (ParamConverter<Integer>) converter;
argsReplacement[index] = intConverter.toString((Integer) arg);
replacementNeeded = true;
break;
case "java.lang.Boolean":
@SuppressWarnings("unchecked")
ParamConverter<Boolean> boolConverter = (ParamConverter<Boolean>) converter;
argsReplacement[index] = boolConverter.toString((Boolean) arg);
replacementNeeded = true;
break;
default:
continue;
}
}
}
} else {
argsReplacement[index] = arg;
}
index++;
}
}
}
}

if (replacementNeeded) {
args = argsReplacement;
}

List<InvocationContextImpl.InterceptorInvocation> chain = interceptorChains.get(method);
if (chain != null) {
// Invoke business method interceptors
return new InvocationContextImpl(target, method, args, chain).proceed();
} else {
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);
// no applicable exception mapper found or applicable mapper returned null
return null;
}
if (cause instanceof ResponseProcessingException) {
ResponseProcessingException rpe = (ResponseProcessingException) cause;
cause = rpe.getCause();
if (cause instanceof RuntimeException) {
throw cause;
}
} else {
if (cause instanceof ProcessingException &&
cause.getCause() instanceof ClientHeaderFillingException) {
throw cause.getCause().getCause();
}
if (cause instanceof RuntimeException) {
throw cause;
}
}
throw e;
}
}
}

private Object invokeRestClientProxyMethod(Object proxy, Method method, Object[] args) {
switch (method.getName()) {
case "getClient":
return client;
case "close":
close();
return null;
default:
throw new IllegalStateException("Unsupported RestClientProxy method: " + method);
}
}

private void close() {
if (closed.compareAndSet(false, true)) {
if (creationalContext != null) {
creationalContext.release();
}
client.close();
}
}

private Type[] getGenericTypes(Class<?> aClass) {
Type[] genericInterfaces = aClass.getGenericInterfaces();
Type[] genericTypes = NO_TYPES;
for (Type genericInterface : genericInterfaces) {
if (genericInterface instanceof ParameterizedType) {
genericTypes = ((ParameterizedType) genericInterface).getActualTypeArguments();
}
}
return genericTypes;
}

private static List<Annotation> getBindings(Annotation[] annotations, BeanManager beanManager) {
if (annotations.length == 0) {
return Collections.emptyList();
}
List<Annotation> bindings = new ArrayList<>();
for (Annotation annotation : annotations) {
if (beanManager.isInterceptorBinding(annotation.annotationType())) {
bindings.add(annotation);
}
}
return bindings;
}

private static BeanManager getBeanManager(Class<?> restClientInterface) {
try {
CDI<Object> current = CDI.current();
return current != null ? current.getBeanManager() : null;
} catch (IllegalStateException e) {
LOGGER.warnf("CDI container is not available - interceptor bindings declared on %s will be ignored",
restClientInterface.getSimpleName());
return null;
}
}

private static Map<Method, List<InvocationContextImpl.InterceptorInvocation>> initInterceptorChains(
BeanManager beanManager, CreationalContext<?> creationalContext, Class<?> restClientInterface) {

Map<Method, List<InvocationContextImpl.InterceptorInvocation>> chains = new HashMap<>();
// Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases
Map<Interceptor<?>, Object> interceptorInstances = new HashMap<>();

List<Annotation> classLevelBindings = getBindings(restClientInterface.getAnnotations(), beanManager);

for (Method method : restClientInterface.getMethods()) {
if (method.isDefault() || Modifier.isStatic(method.getModifiers())) {
continue;
}
List<Annotation> methodLevelBindings = getBindings(method.getAnnotations(), beanManager);

if (!classLevelBindings.isEmpty() || !methodLevelBindings.isEmpty()) {

Annotation[] interceptorBindings = merge(methodLevelBindings, classLevelBindings);

List<Interceptor<?>> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE,
interceptorBindings);
if (!interceptors.isEmpty()) {
List<InvocationContextImpl.InterceptorInvocation> chain = new ArrayList<>();
for (Interceptor<?> interceptor : interceptors) {
chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor,
interceptorInstances.computeIfAbsent(interceptor,
i -> beanManager.getReference(i, i.getBeanClass(), creationalContext))));
}
chains.put(method, chain);
}
}
}
return chains.isEmpty() ? Collections.emptyMap() : chains;
}

private static Annotation[] merge(List<Annotation> methodLevelBindings, List<Annotation> classLevelBindings) {
Set<Class<? extends Annotation>> types = methodLevelBindings.stream()
.map(a -> a.annotationType())
.collect(Collectors.toSet());
List<Annotation> merged = new ArrayList<>(methodLevelBindings);
for (Annotation annotation : classLevelBindings) {
if (!types.contains(annotation.annotationType())) {
merged.add(annotation);
}
}
return merged.toArray(new Annotation[] {});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
import org.jboss.resteasy.microprofile.client.DefaultResponseExceptionMapper;
import org.jboss.resteasy.microprofile.client.ExceptionMapping;
import org.jboss.resteasy.microprofile.client.MethodInjectionFilter;
import org.jboss.resteasy.microprofile.client.ProxyInvocationHandler;
import org.jboss.resteasy.microprofile.client.RestClientListeners;
import org.jboss.resteasy.microprofile.client.RestClientProxy;
import org.jboss.resteasy.microprofile.client.async.AsyncInterceptorRxInvokerProvider;
Expand Down Expand Up @@ -363,7 +362,7 @@ public <T> T build(Class<T> aClass, ClientHttpEngine httpEngine)

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

0 comments on commit 94791ab

Please sign in to comment.