Skip to content
2 changes: 1 addition & 1 deletion src/main/java/graphql/Assert.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static <T> Collection<T> assertNotEmpty(Collection<T> collection) {
return collection;
}

public static <T> Collection<T> assertNotEmpty(Collection<T> collection, Supplier<String> msg) {
public static <C extends Collection<T>, T> C assertNotEmpty(C collection, Supplier<String> msg) {
if (collection == null || collection.isEmpty()) {
throw new AssertException(msg.get());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package graphql.execution;

import graphql.PublicApi;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import static graphql.Assert.assertNotEmpty;
import static graphql.Assert.assertNotNull;

/**
* Delegates to a mapping of {@link Predicate} to the desired {@link DataFetcherExceptionHandler}. The
* corresponding {@link DataFetcherExceptionHandler} of the first {@link Predicate} to match is used. If no match is
* found, then the default is used.
*/
@PublicApi
public class DelegatingDataFetcherExceptionHandler implements DataFetcherExceptionHandler {

private final List<PredicateDelegateMapping> delegates;

private final DataFetcherExceptionHandler defaultHandler;

/**
* Creates a new instance
*
* @param delegates the mapping of {@link Predicate} to {@link DataFetcherExceptionHandler}.
* @param defaultHandler the default {@link DataFetcherExceptionHandler} to use is none of the delegates match
*/
private DelegatingDataFetcherExceptionHandler(List<PredicateDelegateMapping> delegates, DataFetcherExceptionHandler defaultHandler) {
this.delegates = assertNotEmpty(delegates, () -> "delegates can't be empty");
this.defaultHandler = assertNotNull(defaultHandler, () -> "defaultHandler cannot be null");
}

@Override
public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
Throwable exception = handlerParameters.getException();
return delegates.stream()
.filter(mapping -> mapping.getPredicate().test(exception))
.map(PredicateDelegateMapping::getDelegate)
.findFirst()
.orElse(defaultHandler)
.onException(handlerParameters);
}

public static Builder newDelegatingDataFetcherExceptionHandler() {
return new Builder();
}

@PublicApi
public static class Builder {

private List<PredicateDelegateMapping> delegateMappings = new ArrayList<>();

private DataFetcherExceptionHandler defaultHandler = new SimpleDataFetcherExceptionHandler();

/**
* Adds a mapping of the {@link Predicate} to the {@link DataFetcherExceptionHandler}. If the {@link Predicate}
* returns true when {@link DataFetcherExceptionHandlerParameters#getException()} is passed into it, then the
* corresponding {@link DataFetcherExceptionHandler} will be used. Only the first match will be used.
* @param predicate the {@link Predicate} to test if the delegate should be used
* @param delegate the {@link DataFetcherExceptionHandler} to use if the predicate matches
* @return the Builder for further customizations
*/
public Builder addMapping(Predicate<Throwable> predicate, DataFetcherExceptionHandler delegate) {
this.delegateMappings.add(new PredicateDelegateMapping(predicate, delegate));
return this;
}

/**
* Adds a mapping of the {@link Throwable} to {@link DataFetcherExceptionHandler} such that a match occurs if the
* {@link DataFetcherExceptionHandlerParameters#getException()} contains an {@link Exception} that is the same or
* subtype of #matchingThrowableType. Only the first match will be used.
* @param matchingThrowableType the type to test if the Throwable is an instance of
* @param delegate the {@link DataFetcherExceptionHandler} to use if the Throwable is an instance of matchingThrowableType
* @return the Builder for further customizations
*/
public Builder addMapping(Class<? extends Throwable> matchingThrowableType, DataFetcherExceptionHandler delegate) {
assertNotNull(matchingThrowableType, () -> "matchingThrowableType cannot be null");
Predicate<Throwable> predicate = throwable -> matchingThrowableType.isAssignableFrom(throwable.getClass());
return addMapping(predicate, delegate);
}

public Builder defaultHandler(DataFetcherExceptionHandler defaultHandler) {
this.defaultHandler = assertNotNull(defaultHandler, () -> "defaultHandler cannot be null");
return this;
}

public DelegatingDataFetcherExceptionHandler build() {
return new DelegatingDataFetcherExceptionHandler(this.delegateMappings, this.defaultHandler);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont make this mutable - use a builder pattern or have constructors for it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed an update to use a Builder to ensure that DelegatingDataFetcherExceptionHandler is immutable.


/**
* A mapping of a {@link Predicate} to {@link DataFetcherExceptionHandler}.
*/
private static class PredicateDelegateMapping {

private final Predicate<Throwable> predicate;

private final DataFetcherExceptionHandler delegate;

private PredicateDelegateMapping(Predicate<Throwable> predicate, DataFetcherExceptionHandler delegate) {
this.predicate = assertNotNull(predicate, () -> "predicate cannot be null");
this.delegate = assertNotNull(delegate, () -> "delegate cannot be null");
}

private Predicate<Throwable> getPredicate() {
return predicate;
}

private DataFetcherExceptionHandler getDelegate() {
return delegate;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package graphql.execution

import graphql.language.SourceLocation
import graphql.schema.DataFetchingEnvironmentImpl
import spock.lang.Specification

import java.util.function.Predicate

import static graphql.Scalars.GraphQLString
import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo
import static graphql.execution.MergedField.newMergedField
import static graphql.language.Field.newField

class DelegatingDataFetcherExceptionHandlerTest extends Specification {

def "delegates only to matching delegate"() {
given: 'a delegate that matches and a delegate that does not match'
def matchingDelegate = Mock(DataFetcherExceptionHandler)
def notMatchingDelegate = Mock(DataFetcherExceptionHandler)
def expectedResult = Mock(DataFetcherExceptionHandlerResult)
def environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
.build()
def handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters()
.dataFetchingEnvironment(environment)
.exception(new RuntimeException())
.build();
def handler = DelegatingDataFetcherExceptionHandler.newDelegatingDataFetcherExceptionHandler()
.addMapping({ t -> true }, matchingDelegate)
.addMapping({ t -> false }, notMatchingDelegate)
.build()

when: 'onException invoked'
def actualResult = handler.onException(handlerParameters)

then: 'only the matching delegate is invoked'
1 * matchingDelegate.onException(handlerParameters) >> expectedResult
0 * _

and: 'the actual result is equal to the expected result'
actualResult == expectedResult
}

def "default used"() {
given: 'a delegates that do not match'
def location = new SourceLocation(6, 9)
def field = newMergedField(newField("f").sourceLocation(location).build()).build()
def stepInfo = newExecutionStepInfo().path(ResultPath.fromList(["a", "b"])).type(GraphQLString).build()
def notMatchingDelegate = Mock(DataFetcherExceptionHandler)
def delegates = new LinkedHashMap<Predicate<Throwable>, DataFetcherExceptionHandler>()
delegates.put({ t -> false } as Predicate, notMatchingDelegate)
def environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
.mergedField(field)
.executionStepInfo(stepInfo)
.build()
def handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters()
.dataFetchingEnvironment(environment)
.exception(new RuntimeException())
.build();
def handler = DelegatingDataFetcherExceptionHandler.newDelegatingDataFetcherExceptionHandler()
.addMapping({ t -> false }, notMatchingDelegate)
.build()

when: 'onException invoked'
def actualResult = handler.onException(handlerParameters)

then: 'none of the delegates are invoked'
0 * _
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is testing anything??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think then needs updated to be none of the delegates are invoked. This is verify that if none of the delegates match, then none of them are used (the default should be used).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated then description to be accurate.


and: 'the actual result is not null'
actualResult
}

def "default when overridden is used"() {
given: 'a delegates that do not match'
def notMatchingDelegate = Mock(DataFetcherExceptionHandler)
def defaultDelegate = Mock(DataFetcherExceptionHandler)
def expectedResult = Mock(DataFetcherExceptionHandlerResult)
def environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
.build()
def handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters()
.dataFetchingEnvironment(environment)
.exception(new RuntimeException())
.build();
def handler = DelegatingDataFetcherExceptionHandler.newDelegatingDataFetcherExceptionHandler()
.addMapping({ t -> false }, notMatchingDelegate)
.defaultHandler(defaultDelegate)
.build()

when: 'onException invoked'
def actualResult = handler.onException(handlerParameters)

then: 'only the matching delegate is invoked'
1 * defaultDelegate.onException(_) >> expectedResult
0 * _

and: 'the actual result is equal to the expected result'
actualResult == expectedResult
}

def "fromThrowableTypeMapping only delegates to matching delegate"() {
given: 'a delegate that matches and a delegate that does not match'
def matchingDelegate = Mock(DataFetcherExceptionHandler)
def notMatchingDelegate = Mock(DataFetcherExceptionHandler)
def expectedResult = Mock(DataFetcherExceptionHandlerResult)
def environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
.build()
def handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters()
.dataFetchingEnvironment(environment)
.exception(new IllegalStateException())
.build();
def handler = DelegatingDataFetcherExceptionHandler.newDelegatingDataFetcherExceptionHandler()
.addMapping(IllegalStateException, matchingDelegate)
.addMapping(IllegalArgumentException, notMatchingDelegate)
.build()

when: 'onException invoked'
def actualResult = handler.onException(handlerParameters)

then: 'only the matching delegate is invoked'
1 * matchingDelegate.onException(handlerParameters) >> expectedResult
0 * _

and: 'the actual result is equal to the expected result'
actualResult == expectedResult
}

def "fromThrowableTypeMapping only delegates to matching delegate when subclass exception"() {
given: 'a delegate that matches and a delegate that does not match'
def matchingDelegate = Mock(DataFetcherExceptionHandler)
def notMatchingDelegate = Mock(DataFetcherExceptionHandler)
def expectedResult = Mock(DataFetcherExceptionHandlerResult)
def environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
.build()
def handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters()
.dataFetchingEnvironment(environment)
.exception(new IllegalFormatException())
.build();
def handler = DelegatingDataFetcherExceptionHandler.newDelegatingDataFetcherExceptionHandler()
.addMapping(IllegalStateException, notMatchingDelegate)
.addMapping(IllegalArgumentException, matchingDelegate)
.build()

when: 'onException invoked'
def actualResult = handler.onException(handlerParameters)

then: 'only the matching delegate is invoked'
1 * matchingDelegate.onException(handlerParameters) >> expectedResult
0 * _

and: 'the actual result is equal to the expected result'
actualResult == expectedResult
}
}