Merge branch '1.2.x'

This commit is contained in:
rstoyanchev
2024-05-20 13:40:42 +01:00
2 changed files with 67 additions and 33 deletions

View File

@@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import graphql.GraphQLContext;
import io.micrometer.context.ContextSnapshot;
import reactor.core.publisher.Mono;
import org.springframework.core.CoroutinesUtils;
@@ -110,32 +111,22 @@ public abstract class InvocableHandlerMethodSupport extends HandlerMethod {
Object result;
if (this.invokeAsync) {
Callable<Object> callable = () -> method.invoke(getBean(), argValues);
result = adaptCallable(graphQLContext, callable);
result = adaptCallable(graphQLContext, callable, method, argValues);
}
else {
result = method.invoke(getBean(), argValues);
if (this.hasCallableReturnValue && result != null) {
result = adaptCallable(graphQLContext, (Callable<?>) result);
result = adaptCallable(graphQLContext, (Callable<?>) result, method, argValues);
}
}
return result;
}
catch (IllegalArgumentException ex) {
assertTargetBean(method, getBean(), argValues);
String text = (ex.getMessage() != null) ? ex.getMessage() : "Illegal argument";
return Mono.error(new IllegalStateException(formatInvokeError(text, argValues), ex));
return Mono.error(processIllegalArgumentException(argValues, ex, method));
}
catch (InvocationTargetException ex) {
// Unwrap for DataFetcherExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof Error || targetException instanceof Exception) {
return Mono.error(targetException);
}
else {
return Mono.error(new IllegalStateException(
formatInvokeError("Invocation failure", argValues), targetException));
}
return Mono.error(processInvocationTargetException(argValues, ex));
}
catch (Throwable ex) {
return Mono.error(ex);
@@ -155,16 +146,46 @@ public abstract class InvocableHandlerMethodSupport extends HandlerMethod {
return result;
}
private CompletableFuture<?> adaptCallable(GraphQLContext graphQLContext, Callable<?> result) {
return CompletableFuture.supplyAsync(() -> {
@SuppressWarnings("DataFlowIssue")
private CompletableFuture<?> adaptCallable(
GraphQLContext graphQLContext, Callable<?> result, Method method, Object[] argValues) {
CompletableFuture<Object> future = new CompletableFuture<>();
this.executor.execute(() -> {
try {
return ContextSnapshotFactoryHelper.captureFrom(graphQLContext).wrap(result).call();
ContextSnapshot snapshot = ContextSnapshotFactoryHelper.captureFrom(graphQLContext);
Object value = snapshot.wrap((Callable<?>) result).call();
future.complete(value);
}
catch (IllegalArgumentException ex) {
future.completeExceptionally(processIllegalArgumentException(argValues, ex, method));
}
catch (InvocationTargetException ex) {
future.completeExceptionally(processInvocationTargetException(argValues, ex));
}
catch (Exception ex) {
String msg = "Failure in Callable returned from " + getBridgedMethod().toGenericString();
throw new IllegalStateException(msg, ex);
future.completeExceptionally(ex);
}
}, this.executor);
});
return future;
}
private IllegalStateException processIllegalArgumentException(
Object[] argValues, IllegalArgumentException ex, Method method) {
assertTargetBean(method, getBean(), argValues);
String text = (ex.getMessage() != null) ? ex.getMessage() : "Illegal argument";
return new IllegalStateException(formatInvokeError(text, argValues), ex);
}
private Throwable processInvocationTargetException(Object[] argValues, InvocationTargetException ex) {
// Unwrap for DataFetcherExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof Error || targetException instanceof Exception) {
return targetException;
}
String message = formatInvokeError("Invocation failure", argValues);
return new IllegalStateException(message, targetException);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.graphql.data.method.annotation.support;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
@@ -26,17 +27,15 @@ import graphql.GraphQLContext;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -72,17 +71,24 @@ public class DataFetcherHandlerMethodTests {
@Test
void asyncInvocation() throws Exception {
testAsyncInvocation("handleSync", true);
testAsyncInvocation("handleSync", false, true, "A");
}
@Test
void asyncInvocationWithCallableReturnValue() throws Exception {
testAsyncInvocation("handleAndReturnCallable", false);
testAsyncInvocation("handleAndReturnCallable", false, false, "A");
}
private static void testAsyncInvocation(String methodName, boolean invokeAsync) throws Exception {
@Test
void asyncInvocationWithCallableReturnValueError() throws Exception {
testAsyncInvocation("handleAndReturnCallable", true, false, "simulated exception");
}
private static void testAsyncInvocation(
String methodName, boolean raiseError, boolean invokeAsync, String expected) throws Exception {
HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
resolvers.addResolver(Mockito.mock(HandlerMethodArgumentResolver.class));
resolvers.addResolver(new ArgumentMethodArgumentResolver(new GraphQlArgumentBinder()));
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
handlerMethodFor(new TestController(), methodName), resolvers, null,
@@ -90,6 +96,7 @@ public class DataFetcherHandlerMethodTests {
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl
.newDataFetchingEnvironment()
.arguments(Map.of("raiseError", raiseError)) // gh-973
.graphQLContext(GraphQLContext.newContext().build())
.build();
@@ -97,7 +104,10 @@ public class DataFetcherHandlerMethodTests {
assertThat(result).isInstanceOf(CompletableFuture.class);
CompletableFuture<String> future = (CompletableFuture<String>) result;
assertThat(future.get()).isEqualTo("A");
if (raiseError) {
future = future.handle((s, ex) -> ex.getMessage());
}
assertThat(future.get()).isEqualTo(expected);
}
@Test
@@ -144,14 +154,17 @@ public class DataFetcherHandlerMethodTests {
return "Hello, " + name;
}
@Nullable
public String handleSync() {
return "A";
}
@Nullable
public Callable<String> handleAndReturnCallable() {
return () -> "A";
public Callable<String> handleAndReturnCallable(@Argument boolean raiseError) {
return () -> {
if (raiseError) {
throw new IllegalStateException("simulated exception");
}
return "A";
};
}
public CompletableFuture<String> handleAndReturnFuture(@AuthenticationPrincipal User user) {