Skip to content

Commit

Permalink
Allow concurrent CompletionException & ExecutionException to be unwra…
Browse files Browse the repository at this point in the history
…pped for the ExceptionMapper

Signed-off-by: jansupol <[email protected]>
  • Loading branch information
jansupol committed Sep 21, 2020
1 parent 787455a commit c0c2a10
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -489,14 +491,12 @@ private ContainerResponse convertResponse(final Response exceptionResponse) {
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
Expand Down Expand Up @@ -568,8 +568,6 @@ private Response mapException(final Throwable originalThrowable) throws Throwabl

return waeResponse;
}

mappingNotFound = true;
}
// internal mapping
if (throwable instanceof HeaderValueException) {
Expand All @@ -578,18 +576,17 @@ private Response mapException(final Throwable originalThrowable) throws Throwabl
}
}

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;
}
Expand Down Expand Up @@ -1181,4 +1178,91 @@ public void invoke(final ConnectionCallback callback) {
});
}
}

/**
* 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -127,11 +127,29 @@ public void testGetCustomAsync() {
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() {
Expand Down

0 comments on commit c0c2a10

Please sign in to comment.