Commit b0cb7289 authored by Phillip Webb's avatar Phillip Webb

Add general utility to deal with lambda callbacks

Add `LambdaSafe` utility that provides a consistent way to deal with
the problems that can occur when calling lambda based callbacks.

See gh-11584
parent 6582afea
/*
* Copyright 2012-2018 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.boot.util;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.ResolvableType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Utility that can be used to invoke lambdas in a safe way. Primarily designed to help
* support generically typed callbacks where {@link ClassCastException class cast
* exceptions} need to be dealt with due to class erasure.
*
* @author Phillip Webb
* @since 2.0.0
*/
public final class LambdaSafe {
private LambdaSafe() {
}
/**
* Start a call to a single callback instance, dealing with common generic type
* concerns and exceptions.
* @param callbackType the callback type (a {@link FunctionalInterface functional
* interface})
* @param callbackInstance the callback instance (may be a lambda)
* @param argument the primary argument passed to the callback
* @param additionalArguments any additional argument passed to the callback
* @param <C> the callback type
* @param <A> the primary argument type
* @return a {@link Callback} instance that can be invoked.
*/
public static <C, A> Callback<C, A> callback(Class<C> callbackType,
C callbackInstance, A argument, Object... additionalArguments) {
Assert.notNull(callbackType, "CallbackType must not be null");
Assert.notNull(callbackInstance, "CallbackInstance must not be null");
return new Callback<>(callbackType, callbackInstance, argument,
additionalArguments);
}
/**
* Start a call to a single callback instance, dealing with common generic type
* concerns and exceptions.
* @param callbackType the callback type (a {@link FunctionalInterface functional
* interface})
* @param callbackInstances the callback instances (elements may be lambdas)
* @param argument the primary argument passed to the callbacks
* @param additionalArguments any additional argument passed to the callbacks
* @param <C> the callback type
* @param <A> the primary argument type
* @return a {@link Callbacks} instance that can be invoked.
*/
public static <C, A> Callbacks<C, A> callbacks(Class<C> callbackType,
Collection<? extends C> callbackInstances, A argument,
Object... additionalArguments) {
Assert.notNull(callbackType, "CallbackType must not be null");
Assert.notNull(callbackInstances, "CallbackInstances must not be null");
return new Callbacks<C, A>(callbackType, callbackInstances, argument,
additionalArguments);
}
/**
* Abstract base class for lambda safe callbacks.
*/
private static abstract class LambdaSafeCallback<C, A, SELF extends LambdaSafeCallback<C, A, SELF>> {
private final Class<C> callbackType;
private final A argument;
private final Object[] additionalArguments;
private Log logger;
private Filter<C, A> filter = new GenericTypeFilter<C, A>();
protected LambdaSafeCallback(Class<C> callbackType, A argument,
Object[] additionalArguments) {
this.callbackType = callbackType;
this.argument = argument;
this.additionalArguments = additionalArguments;
this.logger = LogFactory.getLog(callbackType);
}
/**
* Use the specified logger source to report any lambda failures.
* @param loggerSource the logger source to use
* @return this instance
*/
public SELF withLogger(Class<?> loggerSource) {
return withLogger(LogFactory.getLog(loggerSource));
}
/**
* Use the specified logger to report any lambda failures.
* @param logger the logger to use
* @return this instance
*/
public SELF withLogger(Log logger) {
Assert.notNull(logger, "Logger must not be null");
this.logger = logger;
return self();
}
/**
* Use a specific filter to determine when a callback should apply. If not
* explicit filter is set filter will be attempted using the generic type on the
* callback type.
* @param filter the filter to use
* @return this instance
*/
public SELF withFilter(Filter<C, A> filter) {
Assert.notNull(filter, "Filter must not be null");
this.filter = filter;
return self();
}
protected final <R> InvocationResult<R> invoke(C callbackInstance,
Supplier<R> supplier) {
if (this.filter.match(this.callbackType, callbackInstance, this.argument,
this.additionalArguments)) {
try {
return InvocationResult.of(supplier.get());
}
catch (ClassCastException ex) {
if (!isLambdaGenericProblem(ex)) {
throw ex;
}
logNonMachingType(callbackInstance, ex);
}
}
return InvocationResult.noResult();
}
private boolean isLambdaGenericProblem(ClassCastException ex) {
return (ex.getMessage() == null
|| startsWithArgumentClassName(ex.getMessage()));
}
private boolean startsWithArgumentClassName(String message) {
Predicate<Object> startsWith = (argument) -> argument != null
&& message.startsWith(argument.getClass().getName());
return startsWith.test(this.argument)
|| Stream.of(this.additionalArguments).anyMatch(startsWith);
}
private void logNonMachingType(C callback, ClassCastException ex) {
if (this.logger.isDebugEnabled()) {
Class<?> expectedType = ResolvableType.forClass(this.callbackType)
.resolveGeneric();
String message = "Non-matching "
+ (expectedType == null ? "type"
: ClassUtils.getShortName(expectedType) + " type")
+ " for callback " + ClassUtils.getShortName(this.callbackType)
+ ": " + callback;
this.logger.debug(message, ex);
}
}
@SuppressWarnings("unchecked")
private SELF self() {
return (SELF) this;
}
}
/**
* Represents a single callback that can be invoked in a lambda safe way.
*
* @param <C> the callback type
* @param <A> the primary argument type
*/
public static final class Callback<C, A>
extends LambdaSafeCallback<C, A, Callback<C, A>> {
private C callbackInstance;
private Callback(Class<C> callbackType, C callbackInstance, A argument,
Object[] additionalArguments) {
super(callbackType, argument, additionalArguments);
this.callbackInstance = callbackInstance;
}
/**
* Invoke the callback instance where the callback method returns void.
* @param invoker the invoker used to invoke the callback
*/
public void invoke(Consumer<C> invoker) {
invoke(this.callbackInstance, () -> {
invoker.accept(this.callbackInstance);
return null;
});
}
/**
* Invoke the callback instance where the callback method returns a result.
* @param invoker the invoker used to invoke the callback
* @param <R> the result type
* @return the result of the invocation (may be {@link InvocationResult#noResult}
* if the callback was not invoked)
*/
public <R> InvocationResult<R> invokeAnd(Function<C, R> invoker) {
return invoke(this.callbackInstance,
() -> invoker.apply(this.callbackInstance));
}
}
/**
* Represents a collection of callbacks that can be invoked in a lambda safe way.
*
* @param <C> the callback type
* @param <A> the primary argument type
*/
public static final class Callbacks<C, A>
extends LambdaSafeCallback<C, A, Callbacks<C, A>> {
private Collection<? extends C> callbackInstances;
private Callbacks(Class<C> callbackType,
Collection<? extends C> callbackInstances, A argument,
Object[] additionalArguments) {
super(callbackType, argument, additionalArguments);
this.callbackInstances = callbackInstances;
}
/**
* Invoke the callback instances where the callback method returns void.
* @param invoker the invoker used to invoke the callback
*/
public void invoke(Consumer<C> invoker) {
Function<C, InvocationResult<Void>> mapper = (callbackInstance) -> invoke(
callbackInstance, () -> {
invoker.accept(callbackInstance);
return null;
});
this.callbackInstances.stream().map(mapper).forEach((result) -> {
});
}
/**
* Invoke the callback instances where the callback method returns a result.
* @param invoker the invoker used to invoke the callback
* @param <R> the result type
* @return the results of the invocation (may be an empty stream if not callbacks
* could be called)
*/
public <R> Stream<R> invokeAnd(Function<C, R> invoker) {
Function<C, InvocationResult<R>> mapper = (callbackInstance) -> invoke(
callbackInstance, () -> invoker.apply(callbackInstance));
return this.callbackInstances.stream().map(mapper)
.filter(InvocationResult::hasResult).map(InvocationResult::get);
}
}
/**
* A filter that can be used to restrict when a callback is used.
*
* @param <C> the callback type
* @param <A> the primary argument type
*/
@FunctionalInterface
interface Filter<C, A> {
/**
* Determine if the given callback matches and should be invoked.
* @param callbackType the callback type (the functional interface)
* @param callbackInstance the callback instance (the implementation)
* @param argument the primary argument
* @param additionalArguments any additional arguments
* @return if the callback matches and should be invoked
*/
boolean match(Class<C> callbackType, C callbackInstance, A argument,
Object[] additionalArguments);
/**
* Return a {@link Filter} that allows all callbacks to be invoked.
* @param <C> the callback type
* @param <A> the primary argument type
* @return an "allow all" filter
*/
static <C, A> Filter<C, A> allowAll() {
return (callbackType, callbackInstance, argument,
additionalArguments) -> true;
}
}
/**
* {@link Filter} that matches when the callback has a single generic and primary
* argument is an instance of it.
*/
private static class GenericTypeFilter<C, A> implements Filter<C, A> {
@Override
public boolean match(Class<C> callbackType, C callbackInstance, A argument,
Object[] additionalArguments) {
ResolvableType type = ResolvableType.forClass(callbackType,
callbackInstance.getClass());
if (type.getGenerics().length == 1 && type.resolveGeneric() != null) {
return type.resolveGeneric().isInstance(argument);
}
return true;
}
}
/**
* The result of a callback which may be a value, {@code null} or absent entirely if
* the callback wasn't suitable. Similar in design to {@link Optional} but allows for
* {@code null} as a valid value.
* @param <R> The result type
*/
public final static class InvocationResult<R> {
private static final InvocationResult<?> NONE = new InvocationResult<Object>(
null);
private final R value;
private InvocationResult(R value) {
this.value = value;
}
/**
* Return true if a result in present.
* @return if a result is present
*/
public boolean hasResult() {
return this != NONE;
}
/**
* Return the result of the invocation or {@code null} if the callback wasn't
* suitable.
* @return the result of the invocation or {@code null}
*/
public R get() {
return this.value;
}
/**
* Return the result of the invocation or the given fallback if the callback
* wasn't suitable.
* @param fallback the fallback to use when there is no result
* @return the result of the invocation or the fallback
*/
public R get(R fallback) {
return (this == NONE ? fallback : this.value);
}
/**
* Create a new {@link InvocationResult} instance with the specified value.
* @param value the value (may be {@code null})
* @param <R> the result type
* @return an {@link InvocationResult}
*/
public static <R> InvocationResult<R> of(R value) {
return new InvocationResult<R>(value);
}
/**
* Return an {@link InvocationResult} instance representing no result.
* @param <R> the result type
* @return an {@link InvocationResult}
*/
@SuppressWarnings("unchecked")
public static <R> InvocationResult<R> noResult() {
return (InvocationResult<R>) NONE;
}
}
}
/*
* Copyright 2012-2018 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.
*/
/**
* Contains miscellaneous utility classes.
*/
package org.springframework.boot.util;
/*
* Copyright 2012-2018 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.boot.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.util.LambdaSafe.Filter;
import org.springframework.boot.util.LambdaSafe.InvocationResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link LambdaSafe}.
*
* @author Phillip Webb
*/
public class LambdaSafeTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void callbackWhenCallbackTypeIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("CallbackType must not be null");
LambdaSafe.callback(null, new Object(), null);
}
@Test
public void callbackWhenCallbackInstanceIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("CallbackInstance must not be null");
LambdaSafe.callback(Object.class, null, null);
}
@Test
public void callbackInvokeWhenNoGenericShouldInvokeCallback() {
NonGenericCallback callbackInstance = mock(NonGenericCallback.class);
String argument = "foo";
LambdaSafe.callback(NonGenericCallback.class, callbackInstance, argument)
.invoke((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeWhenHasGenericShouldInvokeCallback() {
StringCallback callbackInstance = mock(StringCallback.class);
String argument = "foo";
LambdaSafe.callback(GenericCallback.class, callbackInstance, argument)
.invoke((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeWhenHasResolvableGenericMatchShouldInvokeCallback() {
StringBuilderCallback callbackInstance = mock(StringBuilderCallback.class);
StringBuilder argument = new StringBuilder("foo");
LambdaSafe.callback(GenericCallback.class, callbackInstance, argument)
.invoke((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeWhenHasResolvableGenericNonMatchShouldNotInvokeCallback() {
GenericCallback<?> callbackInstance = mock(StringBuilderCallback.class);
String argument = "foo";
LambdaSafe.callback(GenericCallback.class, callbackInstance, argument)
.invoke((c) -> c.handle(argument));
verifyZeroInteractions(callbackInstance);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeWhenLambdaMismatchShouldSwallowException() {
GenericCallback<StringBuilder> callbackInstance = (s) -> {
fail("Should not get here");
};
String argument = "foo";
LambdaSafe.callback(GenericCallback.class, callbackInstance, argument)
.invoke((c) -> c.handle(argument));
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() {
GenericMultiArgCallback<StringBuilder> callbackInstance = (n, s, b) -> {
fail("Should not get here");
};
String argument = "foo";
LambdaSafe.callback(GenericMultiArgCallback.class, callbackInstance, argument)
.invoke((c) -> c.handle(1, argument, false));
}
@Test
public void callbackInvokeAndWhenNoGenericShouldReturnResult() {
NonGenericFactory callbackInstance = mock(NonGenericFactory.class);
String argument = "foo";
given(callbackInstance.handle("foo")).willReturn(123);
InvocationResult<Integer> result = LambdaSafe
.callback(NonGenericFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result.hasResult()).isTrue();
assertThat(result.get()).isEqualTo(123);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeAndWhenHasGenericShouldReturnResult() {
StringFactory callbackInstance = mock(StringFactory.class);
String argument = "foo";
given(callbackInstance.handle("foo")).willReturn(123);
InvocationResult<Integer> result = LambdaSafe
.callback(GenericFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result.hasResult()).isTrue();
assertThat(result.get()).isEqualTo(123);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeAndWhenReturnNullShouldReturnResult() {
StringFactory callbackInstance = mock(StringFactory.class);
String argument = "foo";
given(callbackInstance.handle("foo")).willReturn(null);
InvocationResult<Integer> result = LambdaSafe
.callback(GenericFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result.hasResult()).isTrue();
assertThat(result.get()).isNull();
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeAndWhenHasResolvableGenericMatchShouldReturnResult() {
StringBuilderFactory callbackInstance = mock(StringBuilderFactory.class);
StringBuilder argument = new StringBuilder("foo");
given(callbackInstance.handle(any(StringBuilder.class))).willReturn(123);
InvocationResult<Integer> result = LambdaSafe
.callback(GenericFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
assertThat(result.hasResult()).isTrue();
assertThat(result.get()).isEqualTo(123);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeAndWhenHasResolvableGenericNonMatchShouldReturnNoResult() {
GenericFactory<?> callbackInstance = mock(StringBuilderFactory.class);
String argument = "foo";
InvocationResult<Integer> result = LambdaSafe
.callback(GenericFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result.hasResult()).isFalse();
verifyZeroInteractions(callbackInstance);
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeAndWhenLambdaMismatchShouldSwallowException() {
GenericFactory<StringBuilder> callbackInstance = (s) -> {
fail("Should not get here");
return 123;
};
String argument = "foo";
InvocationResult<Integer> result = LambdaSafe
.callback(GenericFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result.hasResult()).isFalse();
}
@Test
@SuppressWarnings("unchecked")
public void callbackInvokeAndWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() {
GenericMultiArgFactory<StringBuilder> callbackInstance = (n, s, b) -> {
fail("Should not get here");
return 123;
};
String argument = "foo";
InvocationResult<Integer> result = LambdaSafe
.callback(GenericMultiArgFactory.class, callbackInstance, argument)
.invokeAnd((c) -> c.handle(1, argument, false));
assertThat(result.hasResult()).isFalse();
}
@Test
public void callbacksInvokeWhenNoGenericShouldInvokeCallbacks() {
NonGenericCallback callbackInstance = mock(NonGenericCallback.class);
String argument = "foo";
LambdaSafe
.callbacks(NonGenericCallback.class,
Collections.singleton(callbackInstance), argument)
.invoke((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeWhenHasGenericShouldInvokeCallback() {
StringCallback callbackInstance = mock(StringCallback.class);
String argument = "foo";
LambdaSafe.callbacks(GenericCallback.class,
Collections.singleton(callbackInstance), argument)
.invoke((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeWhenHasResolvableGenericMatchShouldInvokeCallback() {
StringBuilderCallback callbackInstance = mock(StringBuilderCallback.class);
StringBuilder argument = new StringBuilder("foo");
LambdaSafe.callbacks(GenericCallback.class,
Collections.singleton(callbackInstance), argument)
.invoke((c) -> c.handle(argument));
verify(callbackInstance).handle(argument);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeWhenHasResolvableGenericNonMatchShouldNotInvokeCallback() {
GenericCallback<?> callbackInstance = mock(StringBuilderCallback.class);
String argument = "foo";
LambdaSafe.callbacks(GenericCallback.class,
Collections.singleton(callbackInstance), argument)
.invoke((c) -> c.handle(null));
verifyZeroInteractions(callbackInstance);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeWhenLambdaMismatchShouldSwallowException() {
GenericCallback<StringBuilder> callbackInstance = (s) -> {
fail("Should not get here");
};
String argument = "foo";
LambdaSafe.callbacks(GenericCallback.class,
Collections.singleton(callbackInstance), argument)
.invoke((c) -> c.handle(argument));
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() {
GenericMultiArgCallback<StringBuilder> callbackInstance = (n, s, b) -> {
fail("Should not get here");
};
String argument = "foo";
LambdaSafe
.callbacks(GenericMultiArgCallback.class,
Collections.singleton(callbackInstance), argument)
.invoke((c) -> c.handle(1, argument, false));
}
@Test
public void callbacksInvokeAndWhenNoGenericShouldReturnResult() {
NonGenericFactory callbackInstance = mock(NonGenericFactory.class);
String argument = "foo";
given(callbackInstance.handle("foo")).willReturn(123);
Stream<Integer> result = LambdaSafe
.callbacks(NonGenericFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result).containsExactly(123);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeAndWhenHasGenericShouldReturnResult() {
StringFactory callbackInstance = mock(StringFactory.class);
String argument = "foo";
given(callbackInstance.handle("foo")).willReturn(123);
Stream<Integer> result = LambdaSafe.callbacks(GenericFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result).containsExactly(123);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeAndWhenReturnNullShouldReturnResult() {
StringFactory callbackInstance = mock(StringFactory.class);
String argument = "foo";
given(callbackInstance.handle("foo")).willReturn(null);
Stream<Integer> result = LambdaSafe.callbacks(GenericFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result).containsExactly((Integer) null);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeAndWhenHasResolvableGenericMatchShouldReturnResult() {
StringBuilderFactory callbackInstance = mock(StringBuilderFactory.class);
StringBuilder argument = new StringBuilder("foo");
given(callbackInstance.handle(any(StringBuilder.class))).willReturn(123);
Stream<Integer> result = LambdaSafe.callbacks(GenericFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result).containsExactly(123);
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeAndWhenHasResolvableGenericNonMatchShouldReturnNoResult() {
GenericFactory<?> callbackInstance = mock(StringBuilderFactory.class);
String argument = "foo";
Stream<Integer> result = LambdaSafe.callbacks(GenericFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeAndWhenLambdaMismatchShouldSwallowException() {
GenericFactory<StringBuilder> callbackInstance = (s) -> {
fail("Should not get here");
return 123;
};
String argument = "foo";
Stream<Integer> result = LambdaSafe.callbacks(GenericFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> (c).handle(argument));
assertThat(result).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeAndWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() {
GenericMultiArgFactory<StringBuilder> callbackInstance = (n, s, b) -> {
fail("Should not get here");
return 123;
};
String argument = "foo";
Stream<Integer> result = LambdaSafe
.callbacks(GenericMultiArgFactory.class,
Collections.singleton(callbackInstance), argument)
.invokeAnd((c) -> c.handle(1, argument, false));
assertThat(result).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void callbacksInvokeWhenMultipleShouldInvokeSuitable() {
List<GenericFactory<?>> callbackInstances = new ArrayList<>();
GenericFactory<String> callback1 = (s) -> 1;
GenericFactory<CharSequence> callback2 = (s) -> 2;
GenericFactory<StringBuilder> callback3 = (s) -> 3;
StringFactory callback4 = mock(StringFactory.class);
given(callback4.handle(any(String.class))).willReturn(4);
StringBuilderFactory callback5 = mock(StringBuilderFactory.class);
given(callback5.handle(any(StringBuilder.class))).willReturn(5);
callbackInstances.add(callback1);
callbackInstances.add(callback2);
callbackInstances.add(callback3);
callbackInstances.add(callback4);
callbackInstances.add(callback5);
String argument = "foo";
Stream<Integer> result = LambdaSafe
.callbacks(GenericFactory.class, callbackInstances, argument)
.invokeAnd((c) -> c.handle(argument));
assertThat(result).containsExactly(1, 2, 4);
}
@Test
@SuppressWarnings("unchecked")
public void callbackWithFilterShouldUseFilter() {
GenericCallback<?> callbackInstance = mock(StringBuilderCallback.class);
String argument = "foo";
LambdaSafe.callback(GenericCallback.class, callbackInstance, argument)
.withFilter(Filter.allowAll()).invoke((c) -> c.handle(null));
verify(callbackInstance).handle(null);
}
@Test
@SuppressWarnings("unchecked")
public void callbackWithLoggerShouldUseLogger() {
Log logger = mock(Log.class);
given(logger.isDebugEnabled()).willReturn(true);
GenericCallback<StringBuilder> callbackInstance = (s) -> {
fail("Should not get here");
};
String argument = "foo";
LambdaSafe.callback(GenericCallback.class, callbackInstance, argument)
.withLogger(logger).invoke((c) -> c.handle(argument));
verify(logger).debug(contains("Non-matching CharSequence type for callback "
+ "LambdaSafeTests.GenericCallback"), any(Throwable.class));
}
interface NonGenericCallback {
void handle(String argument);
}
interface GenericCallback<T extends CharSequence> {
void handle(T argument);
}
interface StringCallback extends GenericCallback<String> {
}
interface StringBuilderCallback extends GenericCallback<StringBuilder> {
}
interface GenericMultiArgCallback<T extends CharSequence> {
void handle(Integer numner, T argument, Boolean bool);
}
interface NonGenericFactory {
Integer handle(String argument);
}
interface GenericFactory<T extends CharSequence> {
Integer handle(T argument);
}
interface StringFactory extends GenericFactory<String> {
}
interface StringBuilderFactory extends GenericFactory<StringBuilder> {
}
interface GenericMultiArgFactory<T extends CharSequence> {
Integer handle(Integer numner, T argument, Boolean bool);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment