Skip to content

Commit a84b26d

Browse files
Allow forcing exception unwrapping even when parent type mappers exist
1 parent 1dadceb commit a84b26d

File tree

7 files changed

+304
-75
lines changed

7 files changed

+304
-75
lines changed

docs/src/main/asciidoc/rest.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,32 @@ public class Mapper {
24222422
24232423
}
24242424
----
2425+
2426+
By default, `@UnwrapException` only unwraps exceptions when no exception mapper exists for the wrapper exception or its parent types.
2427+
If you want to force unwrapping even when a parent type mapper exists, use the `always` attribute:
2428+
2429+
[source,java]
2430+
----
2431+
@UnwrapException(value = {WebApplicationException.class}, always = true)
2432+
public class Mappers {
2433+
2434+
@ServerExceptionMapper
2435+
public Response handleRuntimeException(RuntimeException ex) {
2436+
// Handles RuntimeException and its subclasses
2437+
return Response.status(599).build();
2438+
}
2439+
2440+
@ServerExceptionMapper
2441+
public Response handleJsonProcessingException(JsonProcessingException ex) {
2442+
// This will be called for JsonProcessingException wrapped in WebApplicationException
2443+
// because always=true forces unwrapping even though RuntimeException mapper that would match WebApplicationException exists
2444+
return Response.status(400).entity("Invalid JSON").build();
2445+
}
2446+
2447+
}
2448+
----
2449+
2450+
This is useful when you have a general exception mapper for a parent type (like `RuntimeException`) but want to handle specific wrapped exceptions differently.
24252451
====
24262452

24272453
[NOTE]

extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ public void applicationSpecificUnwrappedExceptions(CombinedIndexBuildItem combin
134134
IndexView index = combinedIndexBuildItem.getIndex();
135135
for (AnnotationInstance instance : index.getAnnotations(UnwrapException.class)) {
136136
AnnotationValue value = instance.value();
137+
AnnotationValue alwaysValue = instance.value("always");
138+
boolean always = alwaysValue != null && alwaysValue.asBoolean();
139+
137140
if (value == null) {
138141
// in this case we need to use the class where the annotation was placed as the exception to be unwrapped
139142

@@ -162,11 +165,11 @@ public void applicationSpecificUnwrappedExceptions(CombinedIndexBuildItem combin
162165
+ classInfo.name() + "'.");
163166
}
164167

165-
producer.produce(new UnwrappedExceptionBuildItem(classInfo.name().toString()));
168+
producer.produce(new UnwrappedExceptionBuildItem(classInfo.name().toString(), always));
166169
} else {
167170
Type[] exceptionTypes = value.asClassArray();
168171
for (Type exceptionType : exceptionTypes) {
169-
producer.produce(new UnwrappedExceptionBuildItem(exceptionType.name().toString()));
172+
producer.produce(new UnwrappedExceptionBuildItem(exceptionType.name().toString(), always));
170173
}
171174
}
172175
}
@@ -186,7 +189,7 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem
186189
exceptions.addBlockingProblem(BlockingOperationNotAllowedException.class);
187190
exceptions.addBlockingProblem(BlockingNotAllowedException.class);
188191
for (UnwrappedExceptionBuildItem bi : unwrappedExceptions) {
189-
exceptions.addUnwrappedException(bi.getThrowableClassName());
192+
exceptions.addUnwrappedException(bi.getThrowableClassName(), bi.isAlways());
190193
}
191194
if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) {
192195
exceptions.addNonBlockingProblem(

extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/UnwrappedExceptionBuildItem.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,30 @@
55
/**
66
* When an {@link Exception} of this type is thrown and no {@code jakarta.ws.rs.ext.ExceptionMapper} exists,
77
* then RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception.
8+
* <p>
9+
* When {@code always} is {@code true}, unwrapping occurs even if an {@code ExceptionMapper} exists for one
10+
* of the class super classes, but not if the exception is directly mapped.
811
*/
912
public final class UnwrappedExceptionBuildItem extends MultiBuildItem {
1013

1114
private final String throwableClassName;
15+
private final boolean always;
1216

1317
public UnwrappedExceptionBuildItem(String throwableClassName) {
18+
this(throwableClassName, false);
19+
}
20+
21+
public UnwrappedExceptionBuildItem(String throwableClassName, boolean always) {
1422
this.throwableClassName = throwableClassName;
23+
this.always = always;
1524
}
1625

1726
public UnwrappedExceptionBuildItem(Class<? extends Throwable> throwableClassName) {
18-
this.throwableClassName = throwableClassName.getName();
27+
this(throwableClassName.getName(), false);
28+
}
29+
30+
public UnwrappedExceptionBuildItem(Class<? extends Throwable> throwableClassName, boolean always) {
31+
this(throwableClassName.getName(), always);
1932
}
2033

2134
@Deprecated(forRemoval = true)
@@ -31,4 +44,8 @@ public Class<? extends Throwable> getThrowableClass() {
3144
public String getThrowableClassName() {
3245
return throwableClassName;
3346
}
47+
48+
public boolean isAlways() {
49+
return always;
50+
}
3451
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/UnwrapException.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,32 @@
88
/**
99
* Used to configure that an exception (or exceptions) should be unwrapped during exception handling.
1010
* <p>
11-
* Unwrapping means that when an {@link Exception} of the configured type is thrown and no
12-
* {@code jakarta.ws.rs.ext.ExceptionMapper} exists,
13-
* then RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception.
11+
* Unwrapping means that when an {@link Exception} of the configured type is thrown,
12+
* RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception.
13+
* <p>
14+
* By default ({@code always = false}), unwrapping only occurs when no {@code jakarta.ws.rs.ext.ExceptionMapper}
15+
* exists for the exception or its parent types.
16+
* <p>
17+
* When {@code always = true}, unwrapping occurs even if an {@code ExceptionMapper} exists for one of the exception
18+
* parent types, but not if the exception is directly mapped.
19+
* <p>
20+
* Example:
21+
*
22+
* <pre>
23+
* &#64;UnwrapException(value = { WebApplicationException.class }, always = true)
24+
* public class ExceptionsMappers {
25+
* &#64;ServerExceptionMapper
26+
* public RestResponse&lt;Error&gt; mapUnhandledException(RuntimeException ex) {
27+
* // Handles RuntimeException and its subclasses
28+
* }
29+
*
30+
* &#64;ServerExceptionMapper
31+
* public RestResponse&lt;Error&gt; mapJsonProcessingException(JsonProcessingException ex) {
32+
* // This will be called for JsonProcessingException wrapped in WebApplicationException
33+
* // because always=true forces unwrapping even though RuntimeException mapper exists
34+
* }
35+
* }
36+
* </pre>
1437
*/
1538
@Retention(RetentionPolicy.RUNTIME)
1639
@Target(ElementType.TYPE)
@@ -20,4 +43,12 @@
2043
* If this is not set, the value is assumed to be the exception class where the annotation is placed
2144
*/
2245
Class<? extends Exception>[] value() default {};
46+
47+
/**
48+
* When {@code true}, the exception is always unwrapped even if an {@code ExceptionMapper} exists
49+
* for the exception or its parent types.
50+
* <p>
51+
* When {@code false} (default), unwrapping only occurs when no {@code ExceptionMapper} exists.
52+
*/
53+
boolean always() default false;
2354
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import java.util.ArrayList;
44
import java.util.Collections;
55
import java.util.HashMap;
6-
import java.util.HashSet;
76
import java.util.List;
87
import java.util.Map;
9-
import java.util.Set;
108
import java.util.function.Function;
119
import java.util.function.Predicate;
1210
import java.util.function.Supplier;
@@ -39,7 +37,7 @@ public class ExceptionMapping {
3937
*/
4038
final List<Predicate<Throwable>> blockingProblemPredicates = new ArrayList<>();
4139
final List<Predicate<Throwable>> nonBlockingProblemPredicate = new ArrayList<>();
42-
final Set<String> unwrappedExceptions = new HashSet<>();
40+
final Map<String, Boolean> unwrappedExceptions = new HashMap<>();
4341

4442
public void addBlockingProblem(Class<? extends Throwable> throwable) {
4543
blockingProblemPredicates.add(new ExceptionTypePredicate(throwable));
@@ -57,11 +55,11 @@ public void addNonBlockingProblem(Predicate<Throwable> predicate) {
5755
nonBlockingProblemPredicate.add(predicate);
5856
}
5957

60-
public void addUnwrappedException(String className) {
61-
unwrappedExceptions.add(className);
58+
public void addUnwrappedException(String className, boolean always) {
59+
unwrappedExceptions.put(className, always);
6260
}
6361

64-
public Set<String> getUnwrappedExceptions() {
62+
public Map<String, Boolean> getUnwrappedExceptions() {
6563
return unwrappedExceptions;
6664
}
6765

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
import java.util.ArrayList;
66
import java.util.Arrays;
77
import java.util.HashMap;
8-
import java.util.HashSet;
98
import java.util.List;
109
import java.util.Map;
1110
import java.util.Objects;
12-
import java.util.Set;
1311
import java.util.function.Predicate;
12+
import java.util.function.Supplier;
1413
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
1515

1616
import jakarta.ws.rs.WebApplicationException;
1717
import jakarta.ws.rs.core.Application;
1818
import jakarta.ws.rs.core.Response;
19+
import jakarta.ws.rs.ext.ExceptionMapper;
1920

2021
import org.jboss.logging.Logger;
2122
import org.jboss.resteasy.reactive.ResteasyReactiveClientProblem;
@@ -35,26 +36,32 @@ public class RuntimeExceptionMapper {
3536
private static Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers;
3637

3738
/**
38-
* Exceptions that indicate an blocking operation was performed on an IO thread.
39+
* Exceptions that indicate a blocking operation was performed on an IO thread.
3940
* <p>
4041
* We have a special log message for this.
4142
*/
4243
private final List<Predicate<Throwable>> blockingProblemPredicates;
4344
private final List<Predicate<Throwable>> nonBlockingProblemPredicate;
44-
private final Set<Class<? extends Throwable>> unwrappedExceptions;
45+
private final Map<Class<? extends Throwable>, Boolean> unwrappedExceptions;
4546

4647
public RuntimeExceptionMapper(ExceptionMapping mapping, ClassLoader classLoader) {
48+
mappers = new HashMap<>();
49+
for (var i : mapping.effectiveMappers().entrySet()) {
50+
mappers.put(loadThrowableClass(i.getKey(), classLoader), i.getValue());
51+
}
52+
blockingProblemPredicates = new ArrayList<>(mapping.blockingProblemPredicates);
53+
nonBlockingProblemPredicate = new ArrayList<>(mapping.nonBlockingProblemPredicate);
54+
unwrappedExceptions = new HashMap<>();
55+
for (Map.Entry<String, Boolean> entry : mapping.getUnwrappedExceptions().entrySet()) {
56+
Class<? extends Throwable> clazz = loadThrowableClass(entry.getKey(), classLoader);
57+
boolean always = entry.getValue();
58+
unwrappedExceptions.put(clazz, always);
59+
}
60+
}
61+
62+
private static Class<? extends Throwable> loadThrowableClass(String className, ClassLoader classLoader) {
4763
try {
48-
mappers = new HashMap<>();
49-
for (var i : mapping.effectiveMappers().entrySet()) {
50-
mappers.put((Class<? extends Throwable>) Class.forName(i.getKey(), false, classLoader), i.getValue());
51-
}
52-
blockingProblemPredicates = new ArrayList<>(mapping.blockingProblemPredicates);
53-
nonBlockingProblemPredicate = new ArrayList<>(mapping.nonBlockingProblemPredicate);
54-
unwrappedExceptions = new HashSet<>();
55-
for (var i : mapping.unwrappedExceptions) {
56-
unwrappedExceptions.add((Class<? extends Throwable>) Class.forName(i, false, classLoader));
57-
}
64+
return (Class<? extends Throwable>) Class.forName(className, false, classLoader);
5865
} catch (ClassNotFoundException e) {
5966
throw new RuntimeException("Could not load exception mapper", e);
6067
}
@@ -226,27 +233,75 @@ private Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwa
226233
return context.getTarget() != null ? context.getTarget().getClassExceptionMappers() : null;
227234
}
228235

229-
@SuppressWarnings({ "unchecked", "rawtypes" })
230236
private <T extends Throwable> Map.Entry<Throwable, jakarta.ws.rs.ext.ExceptionMapper<? extends Throwable>> doGetExceptionMapper(
231237
Class<T> clazz,
232238
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers,
233239
Throwable throwable) {
240+
if (throwable == null) {
241+
return null;
242+
}
243+
Stream<Supplier<AbstractMap.Entry<Throwable, ExceptionMapper<? extends Throwable>>>> mappingStrategies = Stream.of(
244+
// If the exception type is directly mapped, ignore the unwrapping.
245+
() -> buildMapperEntryIfExists(clazz, mappers, throwable),
246+
// Check if the exception should always be unwrapped (if always=true).
247+
// In this case, search mapping for the wrapped exception, ignoring class hierarchy
248+
() -> searchMapperForExceptionsToUnwrap(clazz, mappers, throwable, true),
249+
// Walk up the class hierarchy looking for a mapper for the type
250+
() -> searchMapperInClassHierarchy(clazz, mappers, throwable),
251+
// If no mapper found and exception is marked for unwrapping (if always=false), unwrap it
252+
() -> searchMapperForExceptionsToUnwrap(clazz, mappers, throwable, false));
253+
// Try to get the exception mapper for the exception using strategies in order
254+
return mappingStrategies.map(Supplier::get).filter(Objects::nonNull).findFirst().orElse(null);
255+
}
256+
257+
private static AbstractMap.Entry<Throwable, ExceptionMapper<? extends Throwable>> buildMapperEntryIfExists(
258+
Class<?> klass,
259+
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers,
260+
Throwable throwable) {
261+
ResourceExceptionMapper<? extends Throwable> mapper = mappers.get(klass);
262+
if (mapper != null) {
263+
return new AbstractMap.SimpleEntry<>(
264+
throwable,
265+
mapper.getFactory().createInstance().getInstance());
266+
}
267+
return null;
268+
}
269+
270+
private <T extends Throwable> Map.Entry<Throwable, ExceptionMapper<? extends Throwable>> searchMapperForExceptionsToUnwrap(
271+
Class<T> clazz,
272+
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers,
273+
Throwable throwable,
274+
boolean alwaysRequestedValue) {
275+
if (!unwrappedExceptions.containsKey(clazz)) {
276+
// Exception not defined as unwrappable
277+
return null;
278+
}
279+
boolean always = unwrappedExceptions.get(clazz);
280+
if (alwaysRequestedValue != always) {
281+
// If unwrap is not the requested value ignore the entry
282+
return null;
283+
}
284+
// Do the unwrapping
285+
Throwable cause = throwable.getCause();
286+
if (cause != null) {
287+
return doGetExceptionMapper(cause.getClass(), mappers, cause);
288+
}
289+
return null;
290+
}
291+
292+
private static <T extends Throwable> AbstractMap.Entry<Throwable, ExceptionMapper<? extends Throwable>> searchMapperInClassHierarchy(
293+
Class<T> clazz,
294+
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers,
295+
Throwable throwable) {
234296
Class<?> klass = clazz;
235297
do {
236-
ResourceExceptionMapper<? extends Throwable> mapper = mappers.get(klass);
237-
if (mapper != null) {
238-
return new AbstractMap.SimpleEntry(throwable, mapper.getFactory()
239-
.createInstance().getInstance());
298+
AbstractMap.Entry<Throwable, ExceptionMapper<? extends Throwable>> res = buildMapperEntryIfExists(klass, mappers,
299+
throwable);
300+
if (res != null) {
301+
return res;
240302
}
241303
klass = klass.getSuperclass();
242304
} while (klass != null);
243-
244-
if ((throwable != null) && unwrappedExceptions.contains(clazz)) {
245-
Throwable cause = throwable.getCause();
246-
if (cause != null) {
247-
return doGetExceptionMapper(cause.getClass(), mappers, cause);
248-
}
249-
}
250305
return null;
251306
}
252307

0 commit comments

Comments
 (0)