From 70187e67fe3baa651b8b1aca998d754589d65edd Mon Sep 17 00:00:00 2001 From: Gael Abadin Date: Fri, 19 Jul 2024 17:17:42 +0200 Subject: [PATCH] Create 6828 payara issue reproducer by adding 2 endpoints throwing EJBException wrapped and non-wrapped ValidationExceptions that should be captured by the provided custom EJBExceptionMapper and ValidationExceptionMapper --- .../example/payara/hello/HelloService.java | 6 ++ hello-payara-world-test/pom.xml | 9 ++- .../payara/hello/test/DeploymentIT.java | 42 +++++++++++++- .../test/client/HelloApplicationClient.java | 10 +++- .../src/test/resources/config.properties | 1 + .../src/test/resources/docker-compose.yml | 1 - .../payara/hello/EJBExceptionMapper.java | 55 +++++++++++++++++++ .../example/payara/hello/ErrorResponse.java | 33 +++++++++++ .../example/payara/hello/HelloResource.java | 16 ++++++ .../hello/ValidationExceptionMapper.java | 21 +++++++ pom.xml | 14 ++++- 11 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 hello-payara-world-war/src/main/java/com/example/payara/hello/EJBExceptionMapper.java create mode 100644 hello-payara-world-war/src/main/java/com/example/payara/hello/ErrorResponse.java create mode 100644 hello-payara-world-war/src/main/java/com/example/payara/hello/ValidationExceptionMapper.java diff --git a/hello-payara-world-ejb/src/main/java/com/example/payara/hello/HelloService.java b/hello-payara-world-ejb/src/main/java/com/example/payara/hello/HelloService.java index 733f536..7e76c94 100644 --- a/hello-payara-world-ejb/src/main/java/com/example/payara/hello/HelloService.java +++ b/hello-payara-world-ejb/src/main/java/com/example/payara/hello/HelloService.java @@ -7,6 +7,7 @@ import jakarta.ejb.Singleton; import jakarta.ejb.Startup; import jakarta.inject.Inject; +import jakarta.validation.ValidationException; @Singleton @Startup @@ -29,4 +30,9 @@ public String hello() { return helloStorage.read(id).getMessage(); } + public String helloThrowEJBWrappedValidationException() throws ValidationException { + throw new ValidationException("This should become a 400 after the CustomValidationExceptionMapper maps " + + "this exception that the EJBExceptionMapper rethrows after unwrapping the EJBException containing it"); + } + } \ No newline at end of file diff --git a/hello-payara-world-test/pom.xml b/hello-payara-world-test/pom.xml index 5c64480..0f14320 100644 --- a/hello-payara-world-test/pom.xml +++ b/hello-payara-world-test/pom.xml @@ -179,8 +179,9 @@ pre_deploy_clean_test_environment ${skip.test.deployment} - docker-compose + docker + compose -f target/test-classes/docker-compose.yml down @@ -202,8 +203,9 @@ deploy_test_environment ${skip.test.deployment} - docker-compose + docker + compose -f target/test-classes/docker-compose.yml up @@ -218,8 +220,9 @@ post-integration-test post_undeploy_test_environment - docker-compose + docker + compose -f target/test-classes/docker-compose.yml down diff --git a/hello-payara-world-test/src/test/java/com/example/payara/hello/test/DeploymentIT.java b/hello-payara-world-test/src/test/java/com/example/payara/hello/test/DeploymentIT.java index 612c185..dbb04d6 100644 --- a/hello-payara-world-test/src/test/java/com/example/payara/hello/test/DeploymentIT.java +++ b/hello-payara-world-test/src/test/java/com/example/payara/hello/test/DeploymentIT.java @@ -1,6 +1,8 @@ package com.example.payara.hello.test; import com.example.payara.hello.test.client.HelloApplicationClient; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,6 +14,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import static java.lang.Thread.sleep; import static org.junit.jupiter.api.Assertions.assertEquals; class DeploymentIT { @@ -22,6 +25,8 @@ class DeploymentIT { Properties properties = loadProperties(); String payaraHost = (String) properties.get("payara.host"); int payaraPort = Integer.parseInt((String) properties.get("payara.port")); + int teardownDelay = Integer.parseInt((String) properties.get("payara.teardownDelay")); + static int staticTeardownDelay; private HelloApplicationClient client; @@ -29,14 +34,47 @@ class DeploymentIT { public void before() { client = new HelloApplicationClient("http://"+payaraHost+":"+payaraPort); + staticTeardownDelay = teardownDelay; + + } + + @AfterAll + public static void afterAll() throws InterruptedException { + + // wait before teardown + sleep(staticTeardownDelay); } @Test void testHello(){ client.waitForServiceToBeHealthy(); - assertEquals("Hello, World!", client.helloWorld()); - assertEquals("", parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "SEVERE")); + assertEquals("Hello, World!", client.helloWorld(), "Hello world endpoint response message must match."); + assertEquals("", parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "SEVERE"), + "There should be no SEVERE log traces in the server logs after a hello world endpoint call."); + logger.info(parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "successfully deployed in")); + } + + @Test + void testHelloBadRequestValidationException(){ + client.waitForServiceToBeHealthy(); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), client.helloThrowNotWrappedStatus(), "ValidationException" + + "should be mapped to a bad request response with a 400 HTTP error status response code."); + assertEquals("", parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "SEVERE"), + "There should be no SEVERE log traces in the server logs after a ValidationException is mapped to an" + + " error response."); + logger.info(parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "successfully deployed in")); + } + + @Test + void testHelloBadRequestEJBExceptionWrappedValidationException(){ + client.waitForServiceToBeHealthy(); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), client.helloThrowWrappedStatus(), "EJBException " + + "wrapped ValidationException should be mapped to a bad request response with a 400 HTTP status " + + "error response code."); + assertEquals("", parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "SEVERE"), + "There should be no SEVERE log traces in the server logs after an " + + "EJBException-wrapped ValidationException is mapped to an error response."); logger.info(parseCommandOutput("docker logs test-classes-payara-deployment-test-1", "successfully deployed in")); } diff --git a/hello-payara-world-test/src/test/java/com/example/payara/hello/test/client/HelloApplicationClient.java b/hello-payara-world-test/src/test/java/com/example/payara/hello/test/client/HelloApplicationClient.java index 9145cb8..fd911ca 100644 --- a/hello-payara-world-test/src/test/java/com/example/payara/hello/test/client/HelloApplicationClient.java +++ b/hello-payara-world-test/src/test/java/com/example/payara/hello/test/client/HelloApplicationClient.java @@ -36,7 +36,7 @@ private void check(Response response) { } } - public String helloWorld() { + public String helloWorld() { Response response = client.target(apiUrl + "/hello-world").request().get(); check(response); @@ -72,4 +72,12 @@ public void waitForServiceToBeHealthy() { } throw new RuntimeException("Payara did not become healthy for base url: " + baseUrl); } + + public int helloThrowWrappedStatus() { + return client.target(apiUrl + "/hello-world/hello-throw-ejb-wrapped-validation-exception").request().get().getStatus(); + } + + public int helloThrowNotWrappedStatus() { + return client.target(apiUrl + "/hello-world/hello-throw-not-ejb-wrapped-validation-exception").request().get().getStatus(); + } } diff --git a/hello-payara-world-test/src/test/resources/config.properties b/hello-payara-world-test/src/test/resources/config.properties index 6323e4b..d615ace 100644 --- a/hello-payara-world-test/src/test/resources/config.properties +++ b/hello-payara-world-test/src/test/resources/config.properties @@ -1,3 +1,4 @@ db.port=${db.port} payara.port=${payara.port} payara.host=localhost +payara.teardownDelay=1000 diff --git a/hello-payara-world-test/src/test/resources/docker-compose.yml b/hello-payara-world-test/src/test/resources/docker-compose.yml index 3a5ac6d..88fc7b7 100644 --- a/hello-payara-world-test/src/test/resources/docker-compose.yml +++ b/hello-payara-world-test/src/test/resources/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.3' services: db: image: postgres:latest diff --git a/hello-payara-world-war/src/main/java/com/example/payara/hello/EJBExceptionMapper.java b/hello-payara-world-war/src/main/java/com/example/payara/hello/EJBExceptionMapper.java new file mode 100644 index 0000000..3bbae3b --- /dev/null +++ b/hello-payara-world-war/src/main/java/com/example/payara/hello/EJBExceptionMapper.java @@ -0,0 +1,55 @@ +package com.example.payara.hello; + +import jakarta.ejb.EJBException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.ext.Providers; + +/** + * Maps {@link EJBException} thrown by e.g. EJB services and tries to unwrap and rethrow the exception. + */ +@Provider +public class EJBExceptionMapper implements ExceptionMapper { + + @Context + private Providers providers; + + @Override + public Response toResponse(final EJBException exception) { + + Throwable handledException = exception; + + try { + unwrapEJBException(exception); + } catch (Throwable e) { + // Exception is unwrapped into something not EJBException related + // Try to get the mapper for it + handledException = e; + } + + ExceptionMapper mapper = providers.getExceptionMapper((Class) handledException.getClass()); + return mapper.toResponse(handledException); + } + + /** + * Throws the first nested exception which isn't an {@link EJBException}. + * + * @param wrappingException the EJBException + */ + public static void unwrapEJBException(final EJBException wrappingException) throws Throwable { + Throwable pE = null; + Throwable cE = wrappingException; + while (cE != null && cE.getCause() != pE) { + if (!(cE instanceof EJBException)) { + throw cE; + } + pE = cE; + cE = cE.getCause(); + } + if (cE != null) { + throw cE; + } + } +} diff --git a/hello-payara-world-war/src/main/java/com/example/payara/hello/ErrorResponse.java b/hello-payara-world-war/src/main/java/com/example/payara/hello/ErrorResponse.java new file mode 100644 index 0000000..dd99d6b --- /dev/null +++ b/hello-payara-world-war/src/main/java/com/example/payara/hello/ErrorResponse.java @@ -0,0 +1,33 @@ +package com.example.payara.hello; + +/** + * The generic error response. + */ +public class ErrorResponse { + + // Status code of the HTTP response + private Integer code; + // A human-readable description of the error + private String message; + + public ErrorResponse() { + // NOOP + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/hello-payara-world-war/src/main/java/com/example/payara/hello/HelloResource.java b/hello-payara-world-war/src/main/java/com/example/payara/hello/HelloResource.java index 7c63fe1..fbfe354 100644 --- a/hello-payara-world-war/src/main/java/com/example/payara/hello/HelloResource.java +++ b/hello-payara-world-war/src/main/java/com/example/payara/hello/HelloResource.java @@ -1,6 +1,7 @@ package com.example.payara.hello; import jakarta.inject.Inject; +import jakarta.validation.ValidationException; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -16,4 +17,19 @@ public class HelloResource { public String hello() { return helloService.hello(); } + + @GET + @Path("hello-throw-ejb-wrapped-validation-exception") + @Produces("application/json") + public String helloThrowWrapped() throws ValidationException { + return helloService.helloThrowEJBWrappedValidationException(); + } + + @GET + @Path("hello-throw-not-ejb-wrapped-validation-exception") + @Produces("application/json") + public String helloThrowNotWrapped() { + throw new ValidationException("This is handled correctly by the ValidationExceptionMapper, and 400 is " + + "returned"); + } } \ No newline at end of file diff --git a/hello-payara-world-war/src/main/java/com/example/payara/hello/ValidationExceptionMapper.java b/hello-payara-world-war/src/main/java/com/example/payara/hello/ValidationExceptionMapper.java new file mode 100644 index 0000000..ce4679f --- /dev/null +++ b/hello-payara-world-war/src/main/java/com/example/payara/hello/ValidationExceptionMapper.java @@ -0,0 +1,21 @@ +package com.example.payara.hello; + +import jakarta.validation.ValidationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class ValidationExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final ValidationException exception) { + var errorResponse = new ErrorResponse(); + errorResponse.setCode(Response.Status.BAD_REQUEST.getStatusCode()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(errorResponse) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } + +} diff --git a/pom.xml b/pom.xml index ae139e8..f5c69d5 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,17 @@ ${project.basedir}/src/test/resources - - + + + + org.apache.maven.plugins + maven-ejb-plugin + 3.2.1 + + 3.0 + + + +