Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widen conditions under RESTEasy Reactive Server and RESTEasy Classic Client can work together #35558

Merged
merged 6 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<javax.annotation-api.version>1.3.2</javax.annotation-api.version>
<javax.inject.version>1</javax.inject.version>
<parsson.version>1.1.2</parsson.version>
<resteasy-microprofile.version>2.1.1.Final</resteasy-microprofile.version>
<resteasy-microprofile.version>2.1.4.Final</resteasy-microprofile.version>
<resteasy-spring-web.version>3.0.2.Final</resteasy-spring-web.version>
<resteasy.version>6.2.5.Final</resteasy.version>
<opentracing.version>0.33.0</opentracing.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ void registerProviders(BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
jaxrsProvidersToRegisterBuildItem.useBuiltIn(),
jaxrsProvidersToRegisterBuildItem.getProviders(), jaxrsProvidersToRegisterBuildItem.getContributedProviders());

if (!capabilities.isPresent(Capability.RESTEASY)) {
if (!capabilities.isPresent(Capability.RESTEASY) && !capabilities.isPresent(Capability.RESTEASY_REACTIVE)) {
// ResteasyProviderFactory will use our implementation when accessing instance statically. That's not
// necessary when RESTEasy classic is present as then provider factory with correct provider classes is generated.
restClientRecorder.setResteasyProviderFactoryInstance();
Expand Down
6 changes: 6 additions & 0 deletions extensions/resteasy-classic/rest-client/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-common</artifactId>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-cdi</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
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, beanManager));
new QuarkusProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client, beanManager));
ClientHeaderProviders.registerForClass(aClass, proxy, beanManager);
return proxy;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,20 @@ void deprioritizeLegacyProviders(BuildProducer<MessageBodyReaderOverrideBuildIte
BuildProducer<MessageBodyWriterOverrideBuildItem> writers) {
readers.produce(new MessageBodyReaderOverrideBuildItem(
"org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider", LEGACY_READER_PRIORITY, true));
readers.produce(new MessageBodyReaderOverrideBuildItem("com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider",
readers.produce(new MessageBodyReaderOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider",
LEGACY_READER_PRIORITY, true));
readers.produce(new MessageBodyReaderOverrideBuildItem("com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider",
readers.produce(new MessageBodyReaderOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.JacksonJaxbJsonProvider",
LEGACY_READER_PRIORITY, true));
readers.produce(new MessageBodyReaderOverrideBuildItem("org.keycloak.admin.client.JacksonProvider",
LEGACY_READER_PRIORITY, true));

writers.produce(new MessageBodyWriterOverrideBuildItem(
"org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider", LEGACY_WRITER_PRIORITY, true));
writers.produce(new MessageBodyWriterOverrideBuildItem("com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider",
writers.produce(new MessageBodyWriterOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider",
LEGACY_WRITER_PRIORITY, true));
writers.produce(new MessageBodyWriterOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.json.JacksonJaxbJsonProvider",
LEGACY_WRITER_PRIORITY, true));
writers.produce(new MessageBodyWriterOverrideBuildItem("com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider",
writers.produce(new MessageBodyWriterOverrideBuildItem("org.keycloak.admin.client.JacksonProvider",
LEGACY_WRITER_PRIORITY, true));
}

Expand Down