Allow concurrent CompletionException & ExecutionException to be unwrapped for the ExceptionMapper
Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java b/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
index 906b391..0797653 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -28,7 +28,9 @@
import java.util.List;
import java.util.Map;
import java.util.Queue;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -489,14 +491,12 @@
private Response mapException(final Throwable originalThrowable) throws Throwable {
LOGGER.log(Level.FINER, LocalizationMessages.EXCEPTION_MAPPING_START(), originalThrowable);
- Throwable throwable = originalThrowable;
- boolean inMappable = false;
- boolean mappingNotFound = false;
+ final ThrowableWrap wrap = new ThrowableWrap(originalThrowable);
+ wrap.tryMappableException();
do {
- if (throwable instanceof MappableException) {
- inMappable = true;
- } else if (inMappable || throwable instanceof WebApplicationException) {
+ final Throwable throwable = wrap.getCurrent();
+ if (wrap.isInMappable() || throwable instanceof WebApplicationException) {
// in case ServerProperties.PROCESSING_RESPONSE_ERRORS_ENABLED is true, allow
// wrapped MessageBodyProviderNotFoundException to propagate
if (runtime.processResponseErrors && throwable instanceof InternalServerErrorException
@@ -568,8 +568,6 @@
return waeResponse;
}
-
- mappingNotFound = true;
}
// internal mapping
if (throwable instanceof HeaderValueException) {
@@ -578,18 +576,17 @@
}
}
- if (!inMappable || mappingNotFound) {
+ if (!wrap.isInMappable() || !wrap.isWrapped()) {
// user failures (thrown from Resource methods or provider methods)
// spec: Unchecked exceptions and errors that have not been mapped MUST be re-thrown and allowed to
// propagate to the underlying container.
// not logged on this level.
- throw throwable;
+ throw wrap.getWrappedOrCurrent();
}
- throwable = throwable.getCause();
- } while (throwable != null);
+ } while (wrap.unwrap() != null);
// jersey failures (not thrown from Resource methods or provider methods) -> rethrow
throw originalThrowable;
}
@@ -1181,4 +1178,91 @@
});
}
}
+
+ /**
+ * The structure that holds original {@link Throwable}, top most wrapped {@link Throwable} for the cases where the
+ * exception is to be tried to be mapped but is wrapped in a known wrapping {@link Throwable}, and the current unwrapped
+ * {@link Throwable}. For instance, the original is {@link MappableException}, the wrapped is {@link CompletionException},
+ * and the current is {@code IllegalStateException}.
+ */
+ private static class ThrowableWrap {
+ private final Throwable original;
+ private Throwable wrapped = null;
+ private Throwable current;
+ private boolean inMappable = false;
+
+ private ThrowableWrap(Throwable original) {
+ this.original = original;
+ this.current = original;
+ }
+
+ /**
+ * Gets the original {@link Throwable} to be mapped to an {@link ExceptionMapper}.
+ * @return the original Throwable.
+ */
+ private Throwable getOriginal() {
+ return original;
+ }
+
+ /**
+ * Some exceptions can be unwrapped. If an {@link ExceptionMapper} is not found for them, the original wrapping
+ * {@link Throwable} is to be returned. If the exception was not wrapped, return current.
+ * @return the wrapped or current {@link Throwable}.
+ */
+ private Throwable getWrappedOrCurrent() {
+ return wrapped != null ? wrapped : current;
+ }
+
+ /**
+ * Get current unwrapped {@link Throwable}.
+ * @return current {@link Throwable}.
+ */
+ private Throwable getCurrent() {
+ return current;
+ }
+
+ /**
+ * Check whether the current is a known wrapping exception.
+ * @return true if the current is a known wrapping exception.
+ */
+ private boolean isWrapped() {
+ final boolean isConcurrentWrap =
+ CompletionException.class.isInstance(current) || ExecutionException.class.isInstance(current);
+
+ return isConcurrentWrap;
+ }
+
+ /**
+ * Store the top most wrap exception and return the cause.
+ * @return the cause of the current {@link Throwable}.
+ */
+ private Throwable unwrap() {
+ if (wrapped == null) {
+ wrapped = current;
+ }
+ current = current.getCause();
+ return current;
+ }
+
+ /**
+ * Set flag that the original {@link Throwable} is {@link MappableException} and unwrap the nested {@link Throwable}.
+ * @return true if the original {@link Throwable} is {@link MappableException}.
+ */
+ private boolean tryMappableException() {
+ if (MappableException.class.isInstance(original)) {
+ inMappable = true;
+ current = original.getCause();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return the flag that original {@link Throwable} is {@link MappableException}.
+ * @return true if the original {@link Throwable} is {@link MappableException}.
+ */
+ private boolean isInMappable() {
+ return inMappable;
+ }
+ }
}
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java
index 22f6f89..ee43891 100644
--- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/CompletionStageTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -127,12 +127,30 @@
assertThat(response.readEntity(String.class), is(ENTITY));
}
+ @Test
+ public void test4463() {
+ Response response = target("cs/exceptionally").request().get();
+
+ assertThat(response.getStatus(), is(406));
+ }
+
@Path("/cs")
public static class CompletionStageResource {
private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
@GET
+ @Path("exceptionally")
+ public CompletionStage<String> failAsyncLater() {
+ CompletableFuture<String> fail = new CompletableFuture<>();
+ fail.completeExceptionally(new IllegalStateException("Uh-oh"));
+
+ return fail.exceptionally(ex -> {
+ throw new WebApplicationException("OOPS", Response.Status.NOT_ACCEPTABLE.getStatusCode());
+ });
+ }
+
+ @GET
@Path("/completed")
public CompletionStage<String> getCompleted() {
return CompletableFuture.completedFuture(ENTITY);