diff --git a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java index 197d4948..edae5a3a 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java +++ b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java @@ -24,6 +24,7 @@ import io.github.belgif.rest.problem.api.Input; import io.github.belgif.rest.problem.api.Problem; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemSupport; +import io.github.belgif.rest.problem.ee.resteasy.client.ResteasyProblemSupport; import io.github.belgif.rest.problem.i18n.I18N; import io.github.belgif.rest.problem.it.model.Bean; import io.github.belgif.rest.problem.it.model.ChildModel; @@ -48,8 +49,8 @@ public class FrontendImpl implements Frontend { private final jakarta.ws.rs.client.Client resteasyClient = ProblemSupport.enable(new ResteasyClientBuilderImpl().build()); - private final Backend resteasyProxyClient = ProblemSupport.enable( - new ResteasyClientBuilderImpl().build().target(BASE_URI).proxy(Backend.class)); + private final Backend resteasyProxyClient = ResteasyProblemSupport.proxy( + new ResteasyClientBuilderImpl().build().target(BASE_URI), Backend.class); @EJB private EJBService ejb; diff --git a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java index a35e777d..97e45ed4 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java +++ b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java @@ -24,6 +24,7 @@ import io.github.belgif.rest.problem.api.Input; import io.github.belgif.rest.problem.api.Problem; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemSupport; +import io.github.belgif.rest.problem.ee.resteasy.client.ResteasyProblemSupport; import io.github.belgif.rest.problem.i18n.I18N; import io.github.belgif.rest.problem.it.model.Bean; import io.github.belgif.rest.problem.it.model.ChildModel; @@ -47,8 +48,8 @@ public class FrontendImpl implements Frontend { private final javax.ws.rs.client.Client resteasyClient = ProblemSupport.enable(new ResteasyClientBuilder().build()); - private final Backend resteasyProxyClient = ProblemSupport.enable( - new ResteasyClientBuilder().build().target(BASE_URI).proxy(Backend.class)); + private final Backend resteasyProxyClient = + ResteasyProblemSupport.proxy(new ResteasyClientBuilder().build().target(BASE_URI), Backend.class); @EJB private EJBService ejb; diff --git a/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemClientResponseFilter.java b/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemClientResponseFilter.java index e0714f89..ad38ea4c 100644 --- a/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemClientResponseFilter.java +++ b/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemClientResponseFilter.java @@ -7,7 +7,6 @@ import javax.ws.rs.client.ClientResponseContext; import javax.ws.rs.client.ClientResponseFilter; import javax.ws.rs.core.MediaType; -import javax.ws.rs.ext.Provider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,7 +24,6 @@ * @see ClientResponseFilter * @see ProblemWrapper */ -@Provider public class ProblemClientResponseFilter implements ClientResponseFilter { private static final Logger LOGGER = LoggerFactory.getLogger(ProblemClientResponseFilter.class); diff --git a/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupport.java b/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupport.java index 55644071..ba16fd29 100644 --- a/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupport.java +++ b/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupport.java @@ -41,23 +41,6 @@ public static Client enable(Client client) { return createProxy(Client.class, new ClientInvocationHandler(client)); } - /** - * Enable problem support on the given client (e.g. RESTEasy proxy client). - * - *

- * This causes the client to throw Problem exceptions instead of ProblemWrapper exceptions. - *

- * - * @param client the client - * @param the client type - * @return the problem-enabled client - */ - @SuppressWarnings("unchecked") - public static T enable(T client) { - return (T) Proxy.newProxyInstance(ProblemSupport.class.getClassLoader(), - client.getClass().getInterfaces(), new ProxyInvocationHandler(client)); - } - /** * JDK Dynamic Proxy InvocationHandler for JAX-RS Client. */ @@ -109,31 +92,6 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } - /** - * JDK Dynamic Proxy InvocationHandler for proxy clients. - */ - static final class ProxyInvocationHandler implements InvocationHandler { - - private final Object target; - - ProxyInvocationHandler(Object target) { - this.target = target; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - try { - return method.invoke(target, args); - } catch (InvocationTargetException e) { - if (e.getTargetException() instanceof ProblemWrapper) { - throw ((ProblemWrapper) e.getTargetException()).getProblem(); - } - throw e.getTargetException(); - } - } - - } - @SuppressWarnings("unchecked") private static T createProxy(Class intf, InvocationHandler invocationHandler) { return (T) Proxy.newProxyInstance(ProblemSupport.class.getClassLoader(), diff --git a/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/resteasy/client/ResteasyProblemSupport.java b/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/resteasy/client/ResteasyProblemSupport.java new file mode 100644 index 00000000..4a8ff850 --- /dev/null +++ b/belgif-rest-problem-java-ee-client/src/main/java/io/github/belgif/rest/problem/ee/resteasy/client/ResteasyProblemSupport.java @@ -0,0 +1,66 @@ +package io.github.belgif.rest.problem.ee.resteasy.client; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; + +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemClientResponseFilter; +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemSupport; +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemWrapper; + +/** + * Utility class for enabling problem support on RESTEasy Clients. + */ +public class ResteasyProblemSupport { + + private ResteasyProblemSupport() { + throw new IllegalStateException("Utility class"); + } + + /** + * Create a problem-enabled RESTEasy proxy client. + * + * @param target the ResteasyWebTarget + * @param proxyInterface the service interface + * @return the problem-enabled RESTEasy proxy client + * @param the service interface type + */ + @SuppressWarnings("unchecked") + public static T proxy(ResteasyWebTarget target, Class proxyInterface) { + if (!target.getConfiguration().isRegistered(ProblemClientResponseFilter.class)) { + target.register(ProblemClientResponseFilter.class); + } + T client = target.proxy(proxyInterface); + return (T) Proxy.newProxyInstance(ProblemSupport.class.getClassLoader(), + client.getClass().getInterfaces(), new ProxyInvocationHandler(client)); + } + + /** + * JDK Dynamic Proxy InvocationHandler for RESTEasy proxy clients. + */ + static final class ProxyInvocationHandler implements InvocationHandler { + + private final Object target; + + ProxyInvocationHandler(Object target) { + this.target = target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + return method.invoke(target, args); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof ProblemWrapper) { + throw ((ProblemWrapper) e.getTargetException()).getProblem(); + } + throw e.getTargetException(); + } + } + + } + +} diff --git a/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupportTest.java b/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupportTest.java index 28e2d38c..d756579e 100644 --- a/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupportTest.java +++ b/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/jaxrs/client/ProblemSupportTest.java @@ -20,10 +20,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import io.github.belgif.rest.problem.BadGatewayProblem; import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemSupport.ClientInvocationHandler; -import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemSupport.ProxyInvocationHandler; @ExtendWith(MockitoExtension.class) class ProblemSupportTest { @@ -34,12 +32,8 @@ class ProblemSupportTest { @Mock(strictness = Mock.Strictness.LENIENT) private Configuration configuration; - interface Service { - String test(); - } - @BeforeEach - public void mockConfiguration() { + void mockConfiguration() { when(client.getConfiguration()).thenReturn(configuration); when(configuration.isRegistered(ProblemClientResponseFilter.class)).thenReturn(true); } @@ -217,42 +211,6 @@ void exceptionCreatingProxiedReturnType() { .withMessage("oops"); } - @Test - void unwrapProblemWrapperInProxyClient() { - Service service = mock(Service.class); - Service result = ProblemSupport.enable(service); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(Proxy.getInvocationHandler(result)).isInstanceOf(ProxyInvocationHandler.class); - - doThrow(new ProblemWrapper(new BadGatewayProblem())).when(service).test(); - - assertThatExceptionOfType(BadGatewayProblem.class).isThrownBy(result::test); - } - - @Test - void normalResponseFromProxyClient() { - Service service = mock(Service.class); - Service result = ProblemSupport.enable(service); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(Proxy.getInvocationHandler(result)).isInstanceOf(ProxyInvocationHandler.class); - - when(service.test()).thenReturn("OK"); - - assertThat(result.test()).isEqualTo("OK"); - } - - @Test - void otherExceptionInProxyClient() { - Service service = mock(Service.class); - Service result = ProblemSupport.enable(service); - assertThat(Proxy.isProxyClass(result.getClass())).isTrue(); - assertThat(Proxy.getInvocationHandler(result)).isInstanceOf(ProxyInvocationHandler.class); - - doThrow(new RuntimeException("other")).when(service).test(); - - assertThatRuntimeException().isThrownBy(result::test).withMessage("other"); - } - @Test void configurable() { Client result = ProblemSupport.enable(client); diff --git a/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/resteasy/client/ResteasyProblemSupportTest.java b/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/resteasy/client/ResteasyProblemSupportTest.java new file mode 100644 index 00000000..1c7a84fa --- /dev/null +++ b/belgif-rest-problem-java-ee-client/src/test/java/io/github/belgif/rest/problem/ee/resteasy/client/ResteasyProblemSupportTest.java @@ -0,0 +1,86 @@ +package io.github.belgif.rest.problem.ee.resteasy.client; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Proxy; + +import javax.ws.rs.core.Configuration; + +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.github.belgif.rest.problem.BadGatewayProblem; +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemClientResponseFilter; +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemWrapper; + +@ExtendWith(MockitoExtension.class) +class ResteasyProblemSupportTest { + + @Mock + private ResteasyWebTarget target; + + @Mock + private Configuration configuration; + + @Mock + private Service serviceMock; + + interface Service { + String test(); + } + + @BeforeEach + void mockConfiguration() { + when(target.getConfiguration()).thenReturn(configuration); + when(configuration.isRegistered(ProblemClientResponseFilter.class)).thenReturn(true); + } + + @Test + void proxy() { + when(configuration.isRegistered(ProblemClientResponseFilter.class)).thenReturn(false); + when(target.proxy(Service.class)).thenReturn(serviceMock); + Service service = ResteasyProblemSupport.proxy(target, Service.class); + + assertThat(Proxy.isProxyClass(service.getClass())).isTrue(); + assertThat(Proxy.getInvocationHandler(service)) + .isInstanceOf(ResteasyProblemSupport.ProxyInvocationHandler.class); + + verify(target).register(ProblemClientResponseFilter.class); + } + + @Test + void normalResponseFromProxyClient() { + when(target.proxy(Service.class)).thenReturn(serviceMock); + Service service = ResteasyProblemSupport.proxy(target, Service.class); + + when(serviceMock.test()).thenReturn("OK"); + + assertThat(service.test()).isEqualTo("OK"); + } + + @Test + void unwrapProblemWrapperInProxyClient() { + when(target.proxy(Service.class)).thenReturn(serviceMock); + Service service = ResteasyProblemSupport.proxy(target, Service.class); + + doThrow(new ProblemWrapper(new BadGatewayProblem())).when(serviceMock).test(); + + assertThatExceptionOfType(BadGatewayProblem.class).isThrownBy(service::test); + } + + @Test + void otherExceptionInProxyClient() { + when(target.proxy(Service.class)).thenReturn(serviceMock); + Service service = ResteasyProblemSupport.proxy(target, Service.class); + + doThrow(new RuntimeException("other")).when(serviceMock).test(); + + assertThatRuntimeException().isThrownBy(service::test).withMessage("other"); + } + +} diff --git a/belgif-rest-problem-quarkus-client-deployment/src/main/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessor.java b/belgif-rest-problem-quarkus-client-deployment/src/main/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessor.java index 24216978..8b65d220 100644 --- a/belgif-rest-problem-quarkus-client-deployment/src/main/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessor.java +++ b/belgif-rest-problem-quarkus-client-deployment/src/main/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessor.java @@ -12,6 +12,7 @@ import org.eclipse.microprofile.rest.client.spi.RestClientListener; import io.github.belgif.rest.problem.ee.jaxrs.ProblemObjectMapperContextResolver; +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemClientResponseFilter; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemResponseExceptionMapper; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemRestClientListener; import io.quarkus.deployment.annotations.BuildStep; @@ -55,7 +56,8 @@ ReflectiveClassBuildItem registerRestClientListenerForReflection() { ProblemRestClientListener.class.getName(), ProblemRestClientListener.ClientProblemObjectMapperContextResolver.class.getName(), ProblemObjectMapperContextResolver.class.getName(), - ProblemResponseExceptionMapper.class.getName()) + ProblemResponseExceptionMapper.class.getName(), + ProblemClientResponseFilter.class.getName()) .constructors().methods().fields() .build(); } diff --git a/belgif-rest-problem-quarkus-client-deployment/src/test/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessorTest.java b/belgif-rest-problem-quarkus-client-deployment/src/test/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessorTest.java index 05dac55b..fdf10ea0 100644 --- a/belgif-rest-problem-quarkus-client-deployment/src/test/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessorTest.java +++ b/belgif-rest-problem-quarkus-client-deployment/src/test/java/io/github/belgif/rest/problem/quarkus/deployment/client/ProblemExtensionClientProcessorTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test; import io.github.belgif.rest.problem.ee.jaxrs.ProblemObjectMapperContextResolver; +import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemClientResponseFilter; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemResponseExceptionMapper; import io.github.belgif.rest.problem.ee.jaxrs.client.ProblemRestClientListener; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; @@ -56,7 +57,8 @@ void registerRestClientListenerForReflection() { ProblemRestClientListener.class.getName(), ProblemRestClientListener.ClientProblemObjectMapperContextResolver.class.getName(), ProblemObjectMapperContextResolver.class.getName(), - ProblemResponseExceptionMapper.class.getName()); + ProblemResponseExceptionMapper.class.getName(), + ProblemClientResponseFilter.class.getName()); assertThat(result.isConstructors()).isTrue(); assertThat(result.isMethods()).isTrue(); assertThat(result.isFields()).isTrue(); diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 92e1b7b1..a8021d42 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -303,7 +303,13 @@ Problem support is automatically enabled for MicroProfile REST Client. ==== RESTEasy Proxy Framework -Problem support must be manually enabled for https://docs.jboss.org/resteasy/docs/6.2.10.Final/userguide/#_client_proxies[RESTEasy Proxy Framework] by wrapping the proxy with `client = ProblemSupport.enable(client)`. +Problem support must be manually enabled for https://docs.resteasy.dev/7.0/userguide/#_client_proxies[RESTEasy Proxy Framework] by constructing the proxy with `ResteasyProblemSupport.proxy()`. + +[source,java] +---- +ResteasyWebTarget webTarget = (ResteasyWebTarget) client.target(endpoint); +MyService myService = ResteasyProblemSupport.proxy(webTarget, MyService.class); +---- [[getting-started-quarkus]] === Quarkus @@ -865,7 +871,7 @@ Otherwise, routes to DefaultExceptionMapper. WARNING: In accordance with the spec, any exception thrown by a JAX-RS ClientResponseFilter gets wrapped in a ResponseProcessingException, unless the exception itself is a ResponseProcessingException. For that reason, the ProblemClientResponseFilter wraps the Problem exception in a "ProblemWrapper" class that extends ResponseProcessingException. -** *ProblemSupport:* for programmatically enabling unwrapping of ProblemWrapper to Problem exceptions on a JAX-RS Client (also works for RESTEasy Proxy Framework clients). +** *ProblemSupport:* for programmatically enabling unwrapping of ProblemWrapper to Problem exceptions on a JAX-RS Client + [source,java] ---- @@ -885,6 +891,17 @@ public void setClient(Client client) { * *MicroProfile REST Client integration:* ** *ProblemResponseExceptionMapper:* a ResponseExceptionMapper that converts problem responses to Problem exceptions. ** *ProblemRestClientListener:* a RestClientListener that registers the ProblemObjectMapperContextResolver and ProblemResponseExceptionMapper. + +* *RESTEasy Proxy Framework integration:* + +** *ResteasyProblemSupport:* for creating a problem-enabled RESTEasy client proxy ++ +[source,java] +---- +ResteasyWebTarget webTarget = (ResteasyWebTarget) client.target(endpoint); +MyService myService = ResteasyProblemSupport.proxy(webTarget, MyService.class); +---- + * *Jakarta EE 9+*: the main belgif-rest-problem-java-ee artifact targets Java EE (javax package namespace). A secondary artifact that targets Jakarta EE 9+ (jakarta package namespace) is available with `jakarta`. diff --git a/src/main/asciidoc/release-notes.adoc b/src/main/asciidoc/release-notes.adoc index eb10bf4e..269d9db7 100644 --- a/src/main/asciidoc/release-notes.adoc +++ b/src/main/asciidoc/release-notes.adoc @@ -78,6 +78,11 @@ Note that you can keep using `belgif-rest-problem-java-ee` as before when you wa *Breaking change:* The `jakarta` variant of `belgif-rest-problem-java-ee` no longer exists and should be replaced by `belgif-rest-problem-jakarta-ee`. ==== +Changes impacting RESTEasy clients: + +* Remove `@Provider` annotation on `ProblemClientResponseFilter` so it no longer gets auto-registered for RESTEasy clients +* Introduce `ResteasyProblemSupport` class for creating a problem-enabled RESTEasy proxy client + *belgif-rest-problem-quarkus:* Provide separate modules in case you specifically only want to use problems either client-side or server-side.