mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-27 01:19:02 +00:00
Support matching against exception causes in ExceptionTypeFilter
Prior to this commit, ExceptionTypeFilter only provided support for filtering based on exact matches against exception types; however, some use cases require that filtering be applied to nested causes in a given exception. For example, this functionality is a prerequisite for gh-35583. This commit introduces a new match(Throwable, boolean) method in ExceptionTypeFilter, where the boolean flag enables matching against nested exceptions. See gh-35583 Closes gh-35592
This commit is contained in:
@@ -66,13 +66,59 @@ public class ExceptionTypeFilter extends InstanceFilter<Class<? extends Throwabl
|
||||
|
||||
/**
|
||||
* Determine if the type of the supplied {@code exception} matches this filter.
|
||||
* @param exception the exception to match against
|
||||
* @return {@code true} if this filter matches the supplied exception
|
||||
* @since 7.0
|
||||
* @see #match(Throwable, boolean)
|
||||
*/
|
||||
public boolean match(Throwable exception) {
|
||||
return match(exception, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the type of the supplied {@code exception} matches this filter,
|
||||
* potentially matching against nested causes.
|
||||
* @param exception the exception to match against
|
||||
* @param traverseCauses whether the matching algorithm should recursively
|
||||
* match against nested causes of the exception
|
||||
* @return {@code true} if this filter matches the supplied exception or one
|
||||
* of its nested causes
|
||||
* @since 7.0
|
||||
* @see InstanceFilter#match(Object)
|
||||
*/
|
||||
public boolean match(Throwable exception) {
|
||||
return match(exception.getClass());
|
||||
public boolean match(Throwable exception, boolean traverseCauses) {
|
||||
return (traverseCauses ? matchTraversingCauses(exception) : match(exception.getClass()));
|
||||
}
|
||||
|
||||
private boolean matchTraversingCauses(Throwable exception) {
|
||||
Assert.notNull(exception, "Throwable to match must not be null");
|
||||
|
||||
boolean emptyIncludes = super.includes.isEmpty();
|
||||
boolean emptyExcludes = super.excludes.isEmpty();
|
||||
|
||||
if (emptyIncludes && emptyExcludes) {
|
||||
return super.matchIfEmpty;
|
||||
}
|
||||
if (!emptyExcludes && matchTraversingCauses(exception, super.excludes)) {
|
||||
return false;
|
||||
}
|
||||
return (emptyIncludes || matchTraversingCauses(exception, super.includes));
|
||||
}
|
||||
|
||||
private boolean matchTraversingCauses(
|
||||
Throwable exception, Collection<? extends Class<? extends Throwable>> candidateTypes) {
|
||||
|
||||
for (Class<? extends Throwable> candidateType : candidateTypes) {
|
||||
Throwable current = exception;
|
||||
while (current != null) {
|
||||
if (match(current.getClass(), candidateType)) {
|
||||
return true;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the specified {@code instance} matches the specified
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@@ -35,11 +36,11 @@ import org.jspecify.annotations.Nullable;
|
||||
*/
|
||||
public class InstanceFilter<T> {
|
||||
|
||||
private final Collection<? extends T> includes;
|
||||
protected final Collection<? extends T> includes;
|
||||
|
||||
private final Collection<? extends T> excludes;
|
||||
protected final Collection<? extends T> excludes;
|
||||
|
||||
private final boolean matchIfEmpty;
|
||||
protected final boolean matchIfEmpty;
|
||||
|
||||
|
||||
/**
|
||||
@@ -74,8 +75,8 @@ public class InstanceFilter<T> {
|
||||
public InstanceFilter(@Nullable Collection<? extends T> includes,
|
||||
@Nullable Collection<? extends T> excludes, boolean matchIfEmpty) {
|
||||
|
||||
this.includes = (includes != null ? includes : Collections.emptyList());
|
||||
this.excludes = (excludes != null ? excludes : Collections.emptyList());
|
||||
this.includes = (includes != null ? Collections.unmodifiableCollection(includes) : Set.of());
|
||||
this.excludes = (excludes != null ? Collections.unmodifiableCollection(excludes) : Set.of());
|
||||
this.matchIfEmpty = matchIfEmpty;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ class ExceptionTypeFilterTests {
|
||||
assertMatches(new Error());
|
||||
assertMatches(new Exception());
|
||||
assertMatches(new RuntimeException());
|
||||
|
||||
assertMatchesCause(new Throwable());
|
||||
assertMatchesCause(new Error());
|
||||
assertMatchesCause(new Exception());
|
||||
assertMatchesCause(new RuntimeException());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -67,6 +72,20 @@ class ExceptionTypeFilterTests {
|
||||
assertDoesNotMatch(new Exception());
|
||||
}
|
||||
|
||||
@Test // gh-35583
|
||||
void includesCauseAndSubtypeMatching() {
|
||||
filter = new ExceptionTypeFilter(List.of(IOException.class), null);
|
||||
|
||||
assertMatchesCause(new IOException());
|
||||
assertMatchesCause(new FileNotFoundException());
|
||||
assertMatchesCause(new RuntimeException(new IOException()));
|
||||
assertMatchesCause(new RuntimeException(new FileNotFoundException()));
|
||||
assertMatchesCause(new Exception(new RuntimeException(new IOException())));
|
||||
assertMatchesCause(new Exception(new RuntimeException(new FileNotFoundException())));
|
||||
|
||||
assertDoesNotMatchCause(new Exception());
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludes() {
|
||||
filter = new ExceptionTypeFilter(null, List.of(FileNotFoundException.class, IllegalArgumentException.class));
|
||||
@@ -89,6 +108,20 @@ class ExceptionTypeFilterTests {
|
||||
assertMatches(new Throwable());
|
||||
}
|
||||
|
||||
@Test // gh-35583
|
||||
void excludesCauseAndSubtypeMatching() {
|
||||
filter = new ExceptionTypeFilter(null, List.of(IOException.class));
|
||||
|
||||
assertDoesNotMatchCause(new IOException());
|
||||
assertDoesNotMatchCause(new FileNotFoundException());
|
||||
assertDoesNotMatchCause(new RuntimeException(new IOException()));
|
||||
assertDoesNotMatchCause(new RuntimeException(new FileNotFoundException()));
|
||||
assertDoesNotMatchCause(new Exception(new RuntimeException(new IOException())));
|
||||
assertDoesNotMatchCause(new Exception(new RuntimeException(new FileNotFoundException())));
|
||||
|
||||
assertMatchesCause(new Throwable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void includesAndExcludes() {
|
||||
filter = new ExceptionTypeFilter(List.of(IOException.class), List.of(FileNotFoundException.class));
|
||||
@@ -113,4 +146,16 @@ class ExceptionTypeFilterTests {
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
private void assertMatchesCause(Throwable candidate) {
|
||||
assertThat(this.filter.match(candidate, true))
|
||||
.as("filter '" + this.filter + "' should match " + candidate.getClass().getSimpleName())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
private void assertDoesNotMatchCause(Throwable candidate) {
|
||||
assertThat(this.filter.match(candidate, true))
|
||||
.as("filter '" + this.filter + "' should not match " + candidate.getClass().getSimpleName())
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user