Skip to content

Commit

Permalink
Merge pull request #241 from jamezp/issue239
Browse files Browse the repository at this point in the history
[239] Ensure sub-resources are also proxied.
  • Loading branch information
jamezp authored Jan 4, 2024
2 parents f62b04e + 3efc308 commit 736d8a0
Show file tree
Hide file tree
Showing 8 changed files with 482 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,24 @@

package org.jboss.resteasy.microprofile.client;

import java.io.Closeable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
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;
Expand All @@ -37,6 +45,7 @@
import org.jboss.logging.Logger;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderProviders;

public class ProxyInvocationHandler implements InvocationHandler {

Expand Down Expand Up @@ -64,14 +73,17 @@ public ProxyInvocationHandler(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");
}

Expand Down Expand Up @@ -137,7 +149,30 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
}

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) {
Expand Down Expand Up @@ -167,7 +202,56 @@ 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<Object> providers,
final ResteasyClient client, final BeanManager beanManager) {
return createProxy(resourceInterface, target, true, providers, client, beanManager);
}

/**
* Creates a proxy for the interface.
* <p>
* If {@code addExtendedInterfaces} is set to {@code true}, the proxy will implement the interfaces
* {@link RestClientProxy} and {@link Closeable}.
* </p>
*
* @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<Object> 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;
Expand Down Expand Up @@ -195,4 +279,21 @@ private Type[] getGenericTypes(Class<?> aClass) {
}
return genericTypes;
}

private static ClassLoader getClassLoader(final Class<?> type) {
if (System.getSecurityManager() == null) {
return type.getClassLoader();
}
return AccessController.doPrivileged((PrivilegedAction<ClassLoader>) type::getClassLoader);
}

private static BeanManager getBeanManager() {
try {
CDI<Object> current = CDI.current();
return current != null ? current.getBeanManager() : null;
} catch (IllegalStateException e) {
LOGGER.debug("CDI container is not available", e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
Expand Down Expand Up @@ -88,7 +87,6 @@
import org.jboss.resteasy.concurrent.ContextualExecutors;
import org.jboss.resteasy.microprofile.client.async.AsyncInterceptorRxInvokerProvider;
import org.jboss.resteasy.microprofile.client.async.AsyncInvocationInterceptorThreadContext;
import org.jboss.resteasy.microprofile.client.header.ClientHeaderProviders;
import org.jboss.resteasy.microprofile.client.header.ClientHeadersRequestFilter;
import org.jboss.resteasy.microprofile.client.impl.MpClient;
import org.jboss.resteasy.microprofile.client.impl.MpClientBuilderImpl;
Expand Down Expand Up @@ -378,15 +376,9 @@ public <T> T build(Class<T> 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;

T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces,
new ProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client));
ClientHeaderProviders.registerForClass(aClass, proxy, beanManager);
return proxy;
return aClass.cast(
ProxyInvocationHandler.createProxy(aClass, actualClient, getLocalProviderInstances(), client,
beanManager));
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jboss.resteasy.microprofile.test.client.exception;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.net.URL;

import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.resteasy.microprofile.test.client.exception.resource.ClientRootResource;
import org.jboss.resteasy.microprofile.test.client.exception.resource.ClientSubResource;
import org.jboss.resteasy.microprofile.test.client.exception.resource.ServerResource;
import org.jboss.resteasy.microprofile.test.client.exception.resource.TestException;
import org.jboss.resteasy.microprofile.test.client.exception.resource.TestExceptionMapper;
import org.jboss.resteasy.microprofile.test.util.TestEnvironment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
* Tests client sub-resources
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
@RunWith(Arquillian.class)
@RunAsClient
public class SubResourceTest {

@ArquillianResource
private URL url;

@Deployment
public static WebArchive deployment() {
return TestEnvironment.createWar(SubResourceTest.class)
.addClasses(ServerResource.class);
}

/**
* Creates a REST client with an attached exception mapper. The exception mapper will throw a {@link TestException}.
* This test invokes a call to the root resource.
*
* @throws Exception if a test error occurs
*/
@Test
public void rootResourceExceptionMapper() throws Exception {
try (ClientRootResource root = createClient(TestExceptionMapper.class)) {
try (Response ignore = root.fromRoot()) {
Assert.fail("fromRoot() should have thrown an TestException");
} catch (TestException expected) {
Assert.assertEquals("RootResource failed on purpose", expected.getMessage());
} catch (Exception e) {
failWithException(e, "fromRoot");
}
}
}

/**
* Creates a REST client with an attached exception mapper. The exception mapper will throw a {@link TestException}.
* This test invokes a call to the sub-resource. The sub-resource then invokes an additional call which should also
* result in a {@link TestException} thrown.
*
* @throws Exception if a test error occurs
*/
@Test
public void subResourceExceptionMapper() throws Exception {
try (ClientRootResource root = createClient(TestExceptionMapper.class)) {
final ClientSubResource subResource = root.subResource();
Assert.assertNotNull("The SubResource should not be null", subResource);
try (Response ignore = subResource.fromSub()) {
Assert.fail("fromSub() should have thrown an TestException");
} catch (TestException expected) {
Assert.assertEquals("SubResource failed on purpose", expected.getMessage());
} catch (Exception e) {
failWithException(e, "fromSub");
}
}
}

/**
* This test invokes a call to the sub-resource. The sub-resource then invokes an additional call which should
* return the header value for {@code test-header}.
*
* @throws Exception if a test error occurs
*/
@Test
public void subResourceWithHeader() throws Exception {
try (ClientRootResource root = createClient()) {
final ClientSubResource subResource = root.subResource();
Assert.assertNotNull("The SubResource should not be null", subResource);
try (Response response = subResource.withHeader()) {
Assert.assertEquals(Response.Status.OK, response.getStatusInfo());
final String value = response.readEntity(String.class);
Assert.assertEquals("SubResourceHeader", value);
}
}
}

/**
* This test invokes a call to the sub-resource. The sub-resource then invokes an additional call which should
* return the header value for {@code test-global-header}.
*
* @throws Exception if a test error occurs
*/
@Test
public void subResourceWithGlobalHeader() throws Exception {
try (ClientRootResource root = createClient()) {
final ClientSubResource subResource = root.subResource();
Assert.assertNotNull("The SubResource should not be null", subResource);
try (Response response = subResource.withGlobalHeader()) {
Assert.assertEquals(Response.Status.OK, response.getStatusInfo());
final String value = response.readEntity(String.class);
Assert.assertEquals("GlobalSubResourceHeader", value);
}
}
}

private ClientRootResource createClient() throws URISyntaxException {
return createClient(null);
}

private ClientRootResource createClient(final Class<?> componentType) throws URISyntaxException {
final RestClientBuilder builder = RestClientBuilder.newBuilder()
.baseUri(TestEnvironment.generateUri(url, "test-app"));
if (componentType != null) {
builder.register(componentType);
}
return builder.build(ClientRootResource.class);
}

private static void failWithException(final Exception e, final String methodName) {
final StringWriter writer = new StringWriter();
writer.write(methodName);
writer.write("() should have thrown an TestException. Instead got: ");
writer.write(System.lineSeparator());
e.printStackTrace(new PrintWriter(writer));
Assert.fail(writer.toString());
}
}
Loading

0 comments on commit 736d8a0

Please sign in to comment.