Browse Source

InvocableHandlerMethod and arg resolution updates

General improvements e.g. make use of Java 8 Stream. The main reason
for the refactoring however to tighten error handling. To that extent:

InvocableHandlerMethod turns all exceptions into Reactive Streams
error signals, in effect never allowing any Exceptions to bubble up.

HandlerMethodArgumentResolver may throw an Exception for sync resolution
or produce an error signal via the returned Publisher. Either way the
exception is consistently wrapped with helpful method argument details.
For the latter case using a custom mapError operator.

HandlerMethodArgumentResolver no longer needs to return Optional for
nullable argument values. Instead (for now) the defaultIfEmpty operator
of reactor.rx.Stream operator is used to ensure a default constant value
(called "NO_VALUE") is produced. That way an argument resolver may
produce 0..1 values where 0 means it did not resolve to any value and
that results in null passed as the argument value.

If a HandlerMethodArgumentResolver produces more than one value, all
additional values beyond the first one will be ignored with the help
of a custom "first" operator.

As HandlerMethod is invoked within the map operator, checked exceptions
are not allowed but instead of wrapping it in a runtime exception what
we really need is to unwrap the target exception for exception
resolution purposes. To this end concatMap is used to produce a nested
Publisher or an error Publisher with the unwrapped target exception.

Related to that InvocableHandlerMethod now returns
Publisher<HandlerResult> instead of Publisher<Object> so that no longer
needs to be externally mapped from Object to HandlerResult.

InvocableHandlerMethodTests provides tests for the above scenarios and
verifies the details of resulting error signals.
pull/1111/head
Rossen Stoyanchev 10 years ago
parent
commit
45706422dd
  1. 12
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java
  2. 255
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java
  3. 5
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java
  4. 2
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java
  5. 236
      spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java

12
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java

@ -27,14 +27,18 @@ import org.springframework.http.server.reactive.ServerHttpRequest; @@ -27,14 +27,18 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
*/
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
/**
* The returned Publisher must produce a single value. As Reactive Streams
* does not allow publishing null values, if the value may be {@code null}
* use {@link java.util.Optional#ofNullable(Object)} to wrap it.
* The returned Publisher is expected to produce a single value -- i.e. the
* value to use to invoke the handler method. Any additional values will be
* ignored.
*
* <p>The publisher may also produce zero values if the argument does not
* resolve to any value which will result in passing {@code null} as the
* argument value.
*/
Publisher<Object> resolveArgument(MethodParameter parameter, ServerHttpRequest request);
}

255
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java

@ -19,21 +19,28 @@ package org.springframework.web.reactive.method; @@ -19,21 +19,28 @@ package org.springframework.web.reactive.method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.reactivestreams.Publisher;
import reactor.Publishers;
import reactor.fn.tuple.Tuple;
import reactor.rx.Streams;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult;
/**
@ -41,7 +48,12 @@ import org.springframework.web.method.HandlerMethod; @@ -41,7 +48,12 @@ import org.springframework.web.method.HandlerMethod;
*/
public class InvocableHandlerMethod extends HandlerMethod {
private List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
public static final Publisher<Object[]> NO_ARGS = Publishers.just(new Object[0]);
private final static Object NO_VALUE = new Object();
private List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@ -52,179 +64,132 @@ public class InvocableHandlerMethod extends HandlerMethod { @@ -52,179 +64,132 @@ public class InvocableHandlerMethod extends HandlerMethod {
public void setHandlerMethodArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.argumentResolvers.clear();
this.argumentResolvers.addAll(resolvers);
this.resolvers.clear();
this.resolvers.addAll(resolvers);
}
@Override
protected Method getBridgedMethod() {
return super.getBridgedMethod();
}
public Publisher<Object> invokeForRequest(ServerHttpRequest request,
Object... providedArgs) {
List<Publisher<Object>> argPublishers = getMethodArguments(request, providedArgs);
Publisher<Object[]> argValues = (!argPublishers.isEmpty() ?
Publishers.zip(argPublishers, this::unwrapOptionalArgValues) :
Publishers.just(new Object[0]));
/**
*
* @param request
* @param providedArgs
* @return Publisher that produces a single HandlerResult or an error signal;
* never throws an exception.
*/
public Publisher<HandlerResult> invokeForRequest(ServerHttpRequest request, Object... providedArgs) {
return Publishers.map(argValues, args -> {
if (logger.isTraceEnabled()) {
logger.trace("Invoking [" + getBeanType().getSimpleName() + "." +
getMethod().getName() + "] method with arguments " +
Collections.singletonList(argPublishers));
Publisher<Object[]> argsPublisher = NO_ARGS;
try {
if (!ObjectUtils.isEmpty(getMethodParameters())) {
List<Publisher<Object>> publishers = resolveArguments(request, providedArgs);
argsPublisher = Publishers.zip(publishers, this::initArgs);
argsPublisher = first(argsPublisher);
}
Object returnValue = null;
}
catch (Throwable ex) {
return Publishers.error(ex);
}
return Publishers.concatMap(argsPublisher, args -> {
try {
returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + getMethod().getName() + "] returned " +
"[" + returnValue + "]");
}
Object value = doInvoke(args);
HandlerMethod handlerMethod = InvocableHandlerMethod.this;
ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType());
HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type);
return Publishers.just(handlerResult);
}
catch (InvocationTargetException ex) {
return Publishers.error(ex.getTargetException());
}
catch (Exception ex) {
// TODO: how to best handle error inside map? (also wrapping hides original ex)
throw new IllegalStateException(ex);
catch (Throwable ex) {
String s = getInvocationErrorMessage(args);
return Publishers.error(new IllegalStateException(s));
}
return returnValue;
});
}
private List<Publisher<Object>> getMethodArguments(ServerHttpRequest request,
Object... providedArgs) {
MethodParameter[] parameters = getMethodParameters();
List<Publisher<Object>> valuePublishers = new ArrayList<>(parameters.length);
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
Object value = resolveProvidedArgument(parameter, providedArgs);
if (value != null) {
valuePublishers.add(Publishers.just(value));
continue;
}
boolean resolved = false;
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
private List<Publisher<Object>> resolveArguments(ServerHttpRequest request, Object... providedArgs) {
return Stream.of(getMethodParameters())
.map(parameter -> {
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
if (!ObjectUtils.isEmpty(providedArgs)) {
for (Object providedArg : providedArgs) {
if (parameter.getParameterType().isInstance(providedArg)) {
return Publishers.just(providedArg);
}
}
}
HandlerMethodArgumentResolver resolver = this.resolvers.stream()
.filter(r -> r.supportsParameter(parameter))
.findFirst()
.orElseThrow(() -> getArgError("No resolver for ", parameter, null));
try {
valuePublishers.add(resolver.resolveArgument(parameter, request));
resolved = true;
break;
Publisher<Object> publisher = resolver.resolveArgument(parameter, request);
publisher = mapError(publisher, ex -> getArgError("Error resolving ", parameter, ex));
return Streams.wrap(publisher).defaultIfEmpty(NO_VALUE);
}
catch (Exception ex) {
String msg = buildArgErrorMessage("Error resolving argument", i);
valuePublishers.add(Publishers.error(new IllegalStateException(msg, ex)));
break;
throw getArgError("Error resolving ", parameter, ex);
}
}
}
if (!resolved) {
String msg = buildArgErrorMessage("No suitable resolver for argument", i);
valuePublishers.add(Publishers.error(new IllegalStateException(msg)));
break;
}
}
return valuePublishers;
})
.collect(Collectors.toList());
}
private String buildArgErrorMessage(String message, int index) {
MethodParameter param = getMethodParameters()[index];
message += " [" + index + "] [type=" + param.getParameterType().getName() + "]";
return getDetailedErrorMessage(message);
private IllegalStateException getArgError(String message, MethodParameter param, Throwable cause) {
return new IllegalStateException(message +
"argument [" + param.getParameterIndex() + "] " +
"of type [" + param.getParameterType().getName() + "] " +
"on method [" + getBridgedMethod().toGenericString() + "]", cause);
}
protected String getDetailedErrorMessage(String message) {
return message + "\n" + "HandlerMethod details: \n" +
"Controller [" + getBeanType().getName() + "]\n" +
"Method [" + getBridgedMethod().toGenericString() + "]\n";
}
private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) {
if (providedArgs == null) {
return null;
private Object doInvoke(Object[] args) throws Exception {
if (logger.isTraceEnabled()) {
String target = getBeanType().getSimpleName() + "." + getMethod().getName();
logger.trace("Invoking [" + target + "] method with arguments " + Arrays.toString(args));
}
for (Object providedArg : providedArgs) {
if (parameter.getParameterType().isInstance(providedArg)) {
return providedArg;
}
ReflectionUtils.makeAccessible(getBridgedMethod());
Object returnValue = getBridgedMethod().invoke(getBean(), args);
if (logger.isTraceEnabled()) {
String target = getBeanType().getSimpleName() + "." + getMethod().getName();
logger.trace("Method [" + target + "] returned [" + returnValue + "]");
}
return null;
return returnValue;
}
private void unwrapOptionalArgValues(Object[] args) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Optional) {
Optional optional = (Optional) args[i];
args[i] = optional.isPresent() ? optional.get() : null;
}
}
private String getInvocationErrorMessage(Object[] args) {
String argumentDetails = IntStream.range(0, args.length)
.mapToObj(i -> (args[i] != null ?
"[" + i + "][type=" + args[i].getClass().getName() + "][value=" + args[i] + "]" :
"[" + i + "][null]"))
.collect(Collectors.joining(",", " ", " "));
return "Failed to invoke controller with resolved arguments:" + argumentDetails +
"on method [" + getBridgedMethod().toGenericString() + "]";
}
private Object[] unwrapOptionalArgValues(Tuple tuple) {
Object[] args = new Object[tuple.size()];
for (int i = 0; i < tuple.size(); i++) {
args[i] = tuple.get(i);
if (args[i] instanceof Optional) {
Optional optional = (Optional) args[i];
args[i] = optional.isPresent() ? optional.get() : null;
}
}
return args;
private Object[] initArgs(Tuple tuple) {
return Stream.of(tuple.toArray()).map(o -> o != NO_VALUE ? o : null).toArray();
}
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
String msg = getInvocationErrorMessage("Failed to invoke controller method", args);
throw new IllegalStateException(msg, targetException);
}
}
}
private void assertTargetBean(Method method, Object targetBean, Object[] args) {
Class<?> methodDeclaringClass = method.getDeclaringClass();
Class<?> targetBeanClass = targetBean.getClass();
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
String msg = "The mapped controller method class '" + methodDeclaringClass.getName() +
"' is not an instance of the actual controller bean instance '" +
targetBeanClass.getName() + "'. If the controller requires proxying " +
"(e.g. due to @Transactional), please use class-based proxying.";
throw new IllegalStateException(getInvocationErrorMessage(msg, args));
}
private static <E> Publisher<E> first(Publisher<E> source) {
return Publishers.lift(source, (e, subscriber) -> {
subscriber.onNext(e);
subscriber.onComplete();
});
}
private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
sb.append("Resolved arguments: \n");
for (int i=0; i < resolvedArgs.length; i++) {
sb.append("[").append(i).append("] ");
if (resolvedArgs[i] == null) {
sb.append("[null] \n");
}
else {
sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
sb.append("[value=").append(resolvedArgs[i]).append("]\n");
}
}
return sb.toString();
private static <E> Publisher<E> mapError(Publisher<E> source, Function<Throwable, Throwable> function) {
return Publishers.lift(source, null, (throwable, subscriber) -> {
subscriber.onError(function.apply(throwable));
}, null);
}
}

5
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java

@ -92,10 +92,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin @@ -92,10 +92,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler);
handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType());
Publisher<Object> resultPublisher = handlerMethod.invokeForRequest(request);
return Publishers.map(resultPublisher, result -> new HandlerResult(handlerMethod, result, type));
return handlerMethod.invokeForRequest(request);
}
}

2
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java

@ -50,7 +50,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @@ -50,7 +50,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv
String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName());
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build();
String value = uriComponents.getQueryParams().getFirst(name);
return Publishers.just(Optional.ofNullable(value));
return (value != null ? Publishers.just(value) : Publishers.empty());
}
}

236
spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java

@ -0,0 +1,236 @@ @@ -0,0 +1,236 @@
/*
* Copyright 2002-2015 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.
* 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.springframework.web.reactive.method;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.Publishers;
import reactor.rx.Streams;
import reactor.rx.action.Signal;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Rossen Stoyanchev
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public class InvocableHandlerMethodTests {
private static Log logger = LogFactory.getLog(InvocableHandlerMethodTests.class);
private ServerHttpRequest request;
@Before
public void setUp() throws Exception {
this.request = mock(ServerHttpRequest.class);
}
@Test
public void noArgsMethod() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("noArgs");
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Object value = awaitValue(publisher);
assertEquals("success", value);
}
@Test
public void resolveArgToZeroValues() throws Exception {
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/path"));
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver()));
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Object value = awaitValue(publisher);
assertEquals("success:null", value);
}
@Test
public void resolveArgToOneValue() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
addResolver(hm, Publishers.just("value1"));
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Object value = awaitValue(publisher);
assertEquals("success:value1", value);
}
@Test
public void resolveArgToMultipleValues() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
addResolver(hm, Publishers.from(Arrays.asList("value1", "value2", "value3")));
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
List<Signal<HandlerResult>> signals = awaitSignals(publisher);
assertEquals("Expected only one value: " + signals.toString(), 2, signals.size());
assertEquals(Signal.Type.NEXT, signals.get(0).getType());
assertEquals(Signal.Type.COMPLETE, signals.get(1).getType());
assertEquals("success:value1", signals.get(0).get().getValue());
}
@Test
public void noResolverForArg() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Throwable ex = awaitErrorSignal(publisher);
assertEquals(IllegalStateException.class, ex.getClass());
assertEquals("No resolver for argument [0] of type [java.lang.String] on method " +
"[" + hm.getMethod().toGenericString() + "]", ex.getMessage());
}
@Test
public void resolveArgumentWithThrownException() throws Exception {
HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class);
when(resolver.supportsParameter(any())).thenReturn(true);
when(resolver.resolveArgument(any(), any())).thenThrow(new IllegalStateException("boo"));
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
hm.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver));
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Throwable ex = awaitErrorSignal(publisher);
assertEquals(IllegalStateException.class, ex.getClass());
assertEquals("Exception not wrapped with helpful argument details",
"Error resolving argument [0] of type [java.lang.String] on method " +
"[" + hm.getMethod().toGenericString() + "]", ex.getMessage());
}
@Test
public void resolveArgumentWithErrorSignal() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
addResolver(hm, Publishers.error(new IllegalStateException("boo")));
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Throwable ex = awaitErrorSignal(publisher);
assertEquals(IllegalStateException.class, ex.getClass());
assertEquals("Exception not wrapped with helpful argument details",
"Error resolving argument [0] of type [java.lang.String] on method " +
"[" + hm.getMethod().toGenericString() + "]", ex.getMessage());
}
@Test
public void illegalArgumentExceptionIsWrappedWithHelpfulDetails() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
addResolver(hm, Publishers.just(1));
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Throwable ex = awaitErrorSignal(publisher);
assertEquals(IllegalStateException.class, ex.getClass());
assertEquals("Failed to invoke controller with resolved arguments: " +
"[0][type=java.lang.Integer][value=1] " +
"on method [" + hm.getMethod().toGenericString() + "]", ex.getMessage());
}
@Test
public void invocationTargetExceptionIsUnwrapped() throws Exception {
InvocableHandlerMethod hm = createHandlerMethod("exceptionMethod");
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.request);
Throwable ex = awaitErrorSignal(publisher);
assertEquals(IllegalStateException.class, ex.getClass());
assertEquals("boo", ex.getMessage());
}
private InvocableHandlerMethod createHandlerMethod(String methodName, Class<?>... argTypes) throws Exception {
Object controller = new TestController();
Method method = controller.getClass().getMethod(methodName, argTypes);
return new InvocableHandlerMethod(new HandlerMethod(controller, method));
}
private void addResolver(InvocableHandlerMethod handlerMethod, Publisher<Object> resolvedValue) {
HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class);
when(resolver.supportsParameter(any())).thenReturn(true);
when(resolver.resolveArgument(any(), any())).thenReturn(resolvedValue);
handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver));
}
private Object awaitValue(Publisher<HandlerResult> publisher) throws Exception {
Object object = awaitSignal(publisher, Signal.Type.NEXT).get();
assertEquals(HandlerResult.class, object.getClass());
return ((HandlerResult) object).getValue();
}
private Throwable awaitErrorSignal(Publisher<HandlerResult> publisher) throws Exception {
return awaitSignal(publisher, Signal.Type.ERROR).getThrowable();
}
@SuppressWarnings("unchecked")
private Signal<HandlerResult> awaitSignal(Publisher<HandlerResult> publisher, Signal.Type type) throws Exception {
Signal<HandlerResult> signal = awaitSignals(publisher).get(0);
if (!type.equals(signal.getType()) && signal.isOnError()) {
logger.error("Unexpected error: ", signal.getThrowable());
}
assertEquals("Unexpected signal: " + signal, type, signal.getType());
return signal;
}
private List<Signal<HandlerResult>> awaitSignals(Publisher<HandlerResult> publisher) throws InterruptedException {
return Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS);
}
@SuppressWarnings("unused")
private static class TestController {
public String noArgs() {
return "success";
}
public String singleArg(@RequestParam(required=false) String q) {
return "success:" + q;
}
public void exceptionMethod() {
throw new IllegalStateException("boo");
}
}
}
Loading…
Cancel
Save