From 9e60d9ac8284d0c1a685fd67fa27babfd629b04a Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Mon, 2 Dec 2024 17:42:59 -0600 Subject: [PATCH] Fixes #4312 - Fix BeanParam annotation with body content (#4313) --- .../core/api/WebApplicationInputStream.java | 42 +++++++++++++++++-- .../distribution/BeanParamBean.java | 22 ++++++++-- .../coreprofile/distribution/BeanParamIT.java | 22 +++++++++- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/core/api/src/main/java/cloud/piranha/core/api/WebApplicationInputStream.java b/core/api/src/main/java/cloud/piranha/core/api/WebApplicationInputStream.java index 720eaae48..3291903b4 100644 --- a/core/api/src/main/java/cloud/piranha/core/api/WebApplicationInputStream.java +++ b/core/api/src/main/java/cloud/piranha/core/api/WebApplicationInputStream.java @@ -32,6 +32,13 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -121,12 +128,41 @@ public int read() throws IOException { return -1; } if (inputStream.available() > 0) { + /* + * Because the inputstream indicates we have bytes available we + * read the next byte assuming it won't block. + */ read = inputStream.read(); - index++; - if (index == webApplicationRequest.getContentLength() || read == -1) { - finished = true; + } else { + /* + * Because we do not know if the underlying inputstream can + * block indefinitely we make sure we read from the inputstream + * with a timeout so we do not block the thread indefinitely. + * + * If we do not get a read to succeed within the 30 seconds + * timeout we return -1 to indicate we assume the end of the + * stream has been reached. + */ + ExecutorService executor = Executors.newSingleThreadExecutor(); + + Callable readTask = () -> { + return inputStream.read(); + }; + + Future future = executor.submit(readTask); + + try { + read = future.get(30, TimeUnit.SECONDS); + } catch (TimeoutException | InterruptedException | ExecutionException e) { + read = -1; + } finally { + executor.shutdown(); } } + index++; + if (index == webApplicationRequest.getContentLength() || read == -1) { + finished = true; + } } else { if (inputStream.available() > 0) { read = inputStream.read(); diff --git a/test/coreprofile/integration/src/main/java/cloud/piranha/test/coreprofile/distribution/BeanParamBean.java b/test/coreprofile/integration/src/main/java/cloud/piranha/test/coreprofile/distribution/BeanParamBean.java index 0810c32b6..b65d1d33d 100644 --- a/test/coreprofile/integration/src/main/java/cloud/piranha/test/coreprofile/distribution/BeanParamBean.java +++ b/test/coreprofile/integration/src/main/java/cloud/piranha/test/coreprofile/distribution/BeanParamBean.java @@ -41,19 +41,35 @@ * * @author Manfred Riem (mriem@manorrock.com) */ -@Path("/beanParam") +@Path("beanParam") public class BeanParamBean { /** - * Process BeanParam annotated input. + * Process BeanParam annotated input without content body. * * @param input the input. * @return the response. */ - @POST + @POST + @Path("withoutContent") @Consumes(APPLICATION_FORM_URLENCODED) @Produces(TEXT_PLAIN) public Response beanParamInput(@BeanParam BeanParamInput input) { return Response.ok(input.toString()).build(); } + + /** + * Process BeanParam annotated input with content body. + * + * @param content the content body. + * @param input the input. + * @return the response. + */ + @POST + @Path("withContent") + @Consumes(APPLICATION_FORM_URLENCODED) + @Produces(TEXT_PLAIN) + public Response beanParamInputWithContent(String content, @BeanParam BeanParamInput input) { + return Response.ok(content + "," + input.toString()).build(); + } } diff --git a/test/coreprofile/integration/src/test/java/cloud/piranha/test/coreprofile/distribution/BeanParamIT.java b/test/coreprofile/integration/src/test/java/cloud/piranha/test/coreprofile/distribution/BeanParamIT.java index eed53a213..09e46e361 100644 --- a/test/coreprofile/integration/src/test/java/cloud/piranha/test/coreprofile/distribution/BeanParamIT.java +++ b/test/coreprofile/integration/src/test/java/cloud/piranha/test/coreprofile/distribution/BeanParamIT.java @@ -38,12 +38,12 @@ public class BeanParamIT { @Test - public void testBeanParamAnnotation() throws Exception { + public void testBeanParamAnnotationWithoutContent() throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + System.getProperty("httpPort") - + "/piranha-test-coreprofile-integration/beanParam?queryParam=10")) + + "/piranha-test-coreprofile-integration/beanParam/withoutContent?queryParam=10")) .header("Content-Type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString("formParam=formParam1")) .build(); @@ -54,4 +54,22 @@ public void testBeanParamAnnotation() throws Exception { assertEquals(200, response.statusCode()); assertEquals("UserInput{formParam='formParam1', queryParam=10, contentType='application/x-www-form-urlencoded'}", response.body()); } + + @Test + public void testBeanParamAnnotationWithContent() throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + + System.getProperty("httpPort") + + "/piranha-test-coreprofile-integration/beanParam/withContent?queryParam=10")) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString("CONTENT")) + .build(); + + HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertEquals("CONTENT,UserInput{formParam='null', queryParam=10, contentType='application/x-www-form-urlencoded'}", response.body()); + } }