Skip to content

Commit 670effa

Browse files
committed
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
1 parent 6bc3ce4 commit 670effa

File tree

3 files changed

+99
-7
lines changed

3 files changed

+99
-7
lines changed

spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,59 @@ public ExceptionTypeFilter(@Nullable Collection<? extends Class<? extends Throwa
6666

6767
/**
6868
* Determine if the type of the supplied {@code exception} matches this filter.
69+
* @param exception the exception to match against
70+
* @return {@code true} if this filter matches the supplied exception
6971
* @since 7.0
70-
* @see InstanceFilter#match(Object)
72+
* @see #match(Throwable, boolean)
7173
*/
7274
public boolean match(Throwable exception) {
73-
return match(exception.getClass());
75+
return match(exception, false);
76+
}
77+
78+
/**
79+
* Determine if the type of the supplied {@code exception} matches this filter,
80+
* potentially matching against nested causes.
81+
* @param exception the exception to match against
82+
* @param traverseCauses whether the matching algorithm should recursively
83+
* match against nested causes of the exception
84+
* @return {@code true} if this filter matches the supplied exception or one
85+
* of its nested causes
86+
* @since 7.0
87+
* @see InstanceFilter#match(Object)
88+
*/
89+
public boolean match(Throwable exception, boolean traverseCauses) {
90+
return (traverseCauses ? matchTraversingCauses(exception) : match(exception.getClass()));
7491
}
7592

93+
private boolean matchTraversingCauses(Throwable exception) {
94+
Assert.notNull(exception, "Throwable to match must not be null");
95+
96+
boolean emptyIncludes = super.includes.isEmpty();
97+
boolean emptyExcludes = super.excludes.isEmpty();
98+
99+
if (emptyIncludes && emptyExcludes) {
100+
return super.matchIfEmpty;
101+
}
102+
if (!emptyExcludes && matchTraversingCauses(exception, super.excludes)) {
103+
return false;
104+
}
105+
return (emptyIncludes || matchTraversingCauses(exception, super.includes));
106+
}
107+
108+
private boolean matchTraversingCauses(
109+
Throwable exception, Collection<? extends Class<? extends Throwable>> candidateTypes) {
110+
111+
for (Class<? extends Throwable> candidateType : candidateTypes) {
112+
Throwable current = exception;
113+
while (current != null) {
114+
if (match(current.getClass(), candidateType)) {
115+
return true;
116+
}
117+
current = current.getCause();
118+
}
119+
}
120+
return false;
121+
}
76122

77123
/**
78124
* Determine if the specified {@code instance} matches the specified

spring-core/src/main/java/org/springframework/util/InstanceFilter.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Collection;
2020
import java.util.Collections;
21+
import java.util.Set;
2122

2223
import org.jspecify.annotations.Nullable;
2324

@@ -35,11 +36,11 @@
3536
*/
3637
public class InstanceFilter<T> {
3738

38-
private final Collection<? extends T> includes;
39+
protected final Collection<? extends T> includes;
3940

40-
private final Collection<? extends T> excludes;
41+
protected final Collection<? extends T> excludes;
4142

42-
private final boolean matchIfEmpty;
43+
protected final boolean matchIfEmpty;
4344

4445

4546
/**
@@ -74,8 +75,8 @@ public InstanceFilter(@Nullable Collection<? extends T> includes,
7475
public InstanceFilter(@Nullable Collection<? extends T> includes,
7576
@Nullable Collection<? extends T> excludes, boolean matchIfEmpty) {
7677

77-
this.includes = (includes != null ? includes : Collections.emptyList());
78-
this.excludes = (excludes != null ? excludes : Collections.emptyList());
78+
this.includes = (includes != null ? Collections.unmodifiableCollection(includes) : Set.of());
79+
this.excludes = (excludes != null ? Collections.unmodifiableCollection(excludes) : Set.of());
7980
this.matchIfEmpty = matchIfEmpty;
8081
}
8182

spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ void emptyFilter() {
4343
assertMatches(new Error());
4444
assertMatches(new Exception());
4545
assertMatches(new RuntimeException());
46+
47+
assertMatchesCause(new Throwable());
48+
assertMatchesCause(new Error());
49+
assertMatchesCause(new Exception());
50+
assertMatchesCause(new RuntimeException());
4651
}
4752

4853
@Test
@@ -67,6 +72,20 @@ void includesSubtypeMatching() {
6772
assertDoesNotMatch(new Exception());
6873
}
6974

75+
@Test // gh-35583
76+
void includesCauseAndSubtypeMatching() {
77+
filter = new ExceptionTypeFilter(List.of(IOException.class), null);
78+
79+
assertMatchesCause(new IOException());
80+
assertMatchesCause(new FileNotFoundException());
81+
assertMatchesCause(new RuntimeException(new IOException()));
82+
assertMatchesCause(new RuntimeException(new FileNotFoundException()));
83+
assertMatchesCause(new Exception(new RuntimeException(new IOException())));
84+
assertMatchesCause(new Exception(new RuntimeException(new FileNotFoundException())));
85+
86+
assertDoesNotMatchCause(new Exception());
87+
}
88+
7089
@Test
7190
void excludes() {
7291
filter = new ExceptionTypeFilter(null, List.of(FileNotFoundException.class, IllegalArgumentException.class));
@@ -89,6 +108,20 @@ void excludesSubtypeMatching() {
89108
assertMatches(new Throwable());
90109
}
91110

111+
@Test // gh-35583
112+
void excludesCauseAndSubtypeMatching() {
113+
filter = new ExceptionTypeFilter(null, List.of(IOException.class));
114+
115+
assertDoesNotMatchCause(new IOException());
116+
assertDoesNotMatchCause(new FileNotFoundException());
117+
assertDoesNotMatchCause(new RuntimeException(new IOException()));
118+
assertDoesNotMatchCause(new RuntimeException(new FileNotFoundException()));
119+
assertDoesNotMatchCause(new Exception(new RuntimeException(new IOException())));
120+
assertDoesNotMatchCause(new Exception(new RuntimeException(new FileNotFoundException())));
121+
122+
assertMatchesCause(new Throwable());
123+
}
124+
92125
@Test
93126
void includesAndExcludes() {
94127
filter = new ExceptionTypeFilter(List.of(IOException.class), List.of(FileNotFoundException.class));
@@ -113,4 +146,16 @@ private void assertDoesNotMatch(Throwable candidate) {
113146
.isFalse();
114147
}
115148

149+
private void assertMatchesCause(Throwable candidate) {
150+
assertThat(this.filter.match(candidate, true))
151+
.as("filter '" + this.filter + "' should match " + candidate.getClass().getSimpleName())
152+
.isTrue();
153+
}
154+
155+
private void assertDoesNotMatchCause(Throwable candidate) {
156+
assertThat(this.filter.match(candidate, true))
157+
.as("filter '" + this.filter + "' should not match " + candidate.getClass().getSimpleName())
158+
.isFalse();
159+
}
160+
116161
}

0 commit comments

Comments
 (0)