From dd177e56aa8546097fe3d9fad1400d3a81f20d86 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 20 Aug 2024 16:38:55 -0400 Subject: [PATCH 1/3] Improve the sample by using native-image and adding serialization case --- test-sample-poja/build.gradle | 16 +++++++++++++--- .../http/poja/sample/TestController.java | 8 ++++++++ .../micronaut/http/poja/sample/model/Cactus.java | 10 ++++++++++ .../http/poja/sample/SimpleServerSpec.groovy | 14 +++++++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index d1148b6b3..b638779e9 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -17,16 +17,16 @@ plugins { id("io.micronaut.build.internal.servlet.base") id("application") id("groovy") + id 'org.graalvm.buildtools.native' } dependencies { implementation(projects.micronautHttpPojaApache) implementation(mnLogging.slf4j.simple) - implementation(mn.micronaut.jackson.databind) + implementation(mnSerde.micronaut.serde.jackson) annotationProcessor(mn.micronaut.inject.java) testImplementation(projects.micronautHttpPojaTest) - testImplementation(mn.micronaut.jackson.databind) testImplementation(mnTest.micronaut.test.spock) testImplementation(mn.micronaut.inject.groovy.test) @@ -34,12 +34,22 @@ dependencies { testImplementation(mn.micronaut.inject.groovy) } -run { +application { mainClass.set("io.micronaut.http.poja.sample.Application") +} + +run { standardInput = System.in standardOutput = System.out } +graalvmNative { + binaries.all { + buildArgs.add("--gc=serial") + buildArgs.add("--install-exit-handlers") + } +} + test { useJUnitPlatform() } diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index 96d511c73..1280a2f56 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -23,8 +23,10 @@ import io.micronaut.http.annotation.Delete; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; import io.micronaut.http.annotation.Put; import io.micronaut.http.annotation.Status; +import io.micronaut.http.poja.sample.model.Cactus; /** * A controller for testing. @@ -56,5 +58,11 @@ public final String update(@NonNull String name) { return "Hello, " + name + "!\n"; } + @Get("/cactus") + @Produces(MediaType.APPLICATION_JSON) + public final Cactus getCactus() { + return new Cactus("green", 1); + } + } diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java new file mode 100644 index 000000000..27fdd8772 --- /dev/null +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java @@ -0,0 +1,10 @@ +package io.micronaut.http.poja.sample.model; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public record Cactus( + String color, + int spikeSize +) { +} diff --git a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy index 8fc6cbd2a..2b2c3bb04 100644 --- a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy +++ b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy @@ -6,7 +6,9 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client -import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.poja.sample.model.Cactus +import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.spock.annotation.MicronautTest; import jakarta.inject.Inject; import spock.lang.Specification; @@ -68,4 +70,14 @@ class SimpleServerSpec extends Specification { response.getBody(String.class).get() == "Hello, Andriy!\n" } + void "test GET method with serialization"() { + when: + HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/cactus").header("Host", "h")) + + then: + response.status == HttpStatus.OK + response.contentType.get() == MediaType.APPLICATION_JSON_TYPE + response.getBody(Cactus.class).get() == new Cactus("green", 1) + } + } From f4054553982c3d535e4e13ebad5768f1df40fc84 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 21 Aug 2024 11:48:02 -0400 Subject: [PATCH 2/3] Use Apache APIs for body validation --- .../llhttp/ApacheServerlessApplication.java | 6 +- .../poja/llhttp/ApacheServletHttpRequest.java | 59 +++++++++++-------- .../servlet/undertow/UndertowFactory.java | 2 +- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java index 4afaf6a8b..a1e1f2cc3 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java @@ -17,6 +17,7 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.buffer.ByteBufferFactory; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.codec.MediaTypeCodecRegistry; @@ -26,6 +27,7 @@ import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.servlet.http.ByteArrayBufferFactory; import io.micronaut.servlet.http.ServletHttpHandler; import jakarta.inject.Singleton; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -53,6 +55,7 @@ public class ApacheServerlessApplication private final ConversionService conversionService; private final MediaTypeCodecRegistry codecRegistry; private final ExecutorService ioExecutor; + private final ByteBufferFactory byteBufferFactory; private final ApacheServletConfiguration configuration; /** @@ -68,6 +71,7 @@ public ApacheServerlessApplication(ApplicationContext applicationContext, codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); configuration = applicationContext.getBean(ApacheServletConfiguration.class); + byteBufferFactory = ByteArrayBufferFactory.INSTANCE; } @Override @@ -79,7 +83,7 @@ protected void handleSingleRequest( ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); try { ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, response, configuration + in, conversionService, codecRegistry, ioExecutor, byteBufferFactory, response, configuration ); servletHttpHandler.service(exchange); } catch (ApacheServletBadRequestException e) { diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index d4683758e..aadccf054 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -18,32 +18,34 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.buffer.ByteBufferFactory; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpParameters; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.body.ByteBody; +import io.micronaut.http.body.stream.InputStreamByteBody; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; import io.micronaut.http.poja.PojaHttpRequest; import io.micronaut.http.poja.llhttp.exception.ApacheServletBadRequestException; -import io.micronaut.http.poja.util.LimitingInputStream; import io.micronaut.http.poja.util.MultiValueHeaders; import io.micronaut.http.poja.util.MultiValuesQueryParameters; import io.micronaut.http.simple.cookies.SimpleCookies; -import io.micronaut.servlet.http.body.InputStreamByteBody; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.impl.io.ChunkedInputStream; +import org.apache.hc.core5.http.impl.io.ContentLengthInputStream; import org.apache.hc.core5.http.impl.io.DefaultHttpRequestParser; import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; +import org.apache.hc.core5.http.io.entity.EmptyInputStream; import org.apache.hc.core5.net.URIBuilder; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -67,6 +69,8 @@ @Internal public final class ApacheServletHttpRequest extends PojaHttpRequest { + private static final String TRANSFER_ENCODING_CHUNKED = "chunked"; + private final ClassicHttpRequest request; private final HttpMethod method; @@ -92,6 +96,7 @@ public ApacheServletHttpRequest( ConversionService conversionService, MediaTypeCodecRegistry codecRegistry, ExecutorService ioExecutor, + ByteBufferFactory byteBufferFactory, ApacheServletHttpResponse response, ApacheServletConfiguration configuration ) { @@ -119,29 +124,33 @@ public ApacheServletHttpRequest( long contentLength = getContentLength(); OptionalLong optionalContentLength = contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(); - try { - InputStream bodyStream = inputStream; - if (sessionInputBuffer.length() > 0) { - byte[] data = new byte[sessionInputBuffer.length()]; - sessionInputBuffer.read(data, inputStream); - - bodyStream = new CombinedInputStream( - new ByteArrayInputStream(data), - inputStream - ); - } - if (contentLength > 0) { - bodyStream = new LimitingInputStream(bodyStream, contentLength); - } else { - // Empty - bodyStream = new ByteArrayInputStream(new byte[0]); - } - byteBody = InputStreamByteBody.create( - bodyStream, optionalContentLength, ioExecutor - ); - } catch (IOException e) { - throw new ApacheServletBadRequestException("Could not parse request body", e); + InputStream bodyStream = createBodyStream(inputStream, contentLength, sessionInputBuffer); + byteBody = InputStreamByteBody.create( + bodyStream, optionalContentLength, ioExecutor, byteBufferFactory + ); + } + + /** + * Create body stream. + * Based on org.apache.hc.core5.http.impl.io.BHttpConnectionBase#createContentOutputStream. + * + * @param inputStream The input stream + * @param contentLength The content length + * @param sessionInputBuffer The input buffer + * @return The body stream + */ + private InputStream createBodyStream(InputStream inputStream, long contentLength, SessionInputBufferImpl sessionInputBuffer) { + InputStream bodyStream; + if (contentLength > 0) { + bodyStream = new ContentLengthInputStream(sessionInputBuffer, inputStream, contentLength); + } else if (contentLength == 0) { + bodyStream = EmptyInputStream.INSTANCE; + } else if (TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(headers.get(HttpHeaders.TRANSFER_ENCODING))) { + bodyStream = new ChunkedInputStream(sessionInputBuffer, inputStream); + } else { + bodyStream = EmptyInputStream.INSTANCE; } + return bodyStream; } @Override diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index 3576ef784..8ddbf930f 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -263,7 +263,7 @@ protected Undertow undertowServer(Undertow.Builder builder) { * * @param servletConfiguration The servlet configuration. * @return The deployment info - * @deprecated Use {@link ##deploymentInfo(MicronautServletConfiguration, Collection)} + * @deprecated Use {@link #deploymentInfo(MicronautServletConfiguration, Collection)} */ @Deprecated(forRemoval = true, since = "4.8.0") protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration) { From 9e9a23c96573b58aa32ec83667ed9acf0e10397c Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 23 Aug 2024 12:59:16 -0400 Subject: [PATCH 3/3] Rewrite test to Java --- test-sample-poja/build.gradle | 6 +- .../http/poja/sample/SimpleServerSpec.groovy | 83 ------------------ .../http/poja/sample/SimpleServerTest.java | 87 +++++++++++++++++++ 3 files changed, 89 insertions(+), 87 deletions(-) delete mode 100644 test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy create mode 100644 test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index b638779e9..1c3390f9a 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -16,7 +16,6 @@ plugins { id("io.micronaut.build.internal.servlet.base") id("application") - id("groovy") id 'org.graalvm.buildtools.native' } @@ -27,11 +26,10 @@ dependencies { annotationProcessor(mn.micronaut.inject.java) testImplementation(projects.micronautHttpPojaTest) - testImplementation(mnTest.micronaut.test.spock) + testImplementation(mnTest.micronaut.test.junit5) - testImplementation(mn.micronaut.inject.groovy.test) + testAnnotationProcessor(mn.micronaut.inject.java.test) testImplementation(mn.micronaut.inject.java) - testImplementation(mn.micronaut.inject.groovy) } application { diff --git a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy deleted file mode 100644 index 2b2c3bb04..000000000 --- a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package io.micronaut.http.poja.sample - -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus -import io.micronaut.http.MediaType; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.http.poja.sample.model.Cactus -import io.micronaut.serde.annotation.Serdeable; -import io.micronaut.test.extensions.spock.annotation.MicronautTest; -import jakarta.inject.Inject; -import spock.lang.Specification; - -@MicronautTest -class SimpleServerSpec extends Specification { - - @Inject - @Client("/") - HttpClient client - - void "test GET method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/").header("Host", "h")) - - then: - response.status == HttpStatus.OK - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == 'Hello, Micronaut Without Netty!\n' - } - - void "test invalid GET method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/test/invalid").header("Host", "h")) - - then: - var e = thrown(HttpClientResponseException) - e.status == HttpStatus.NOT_FOUND - e.response.contentType.get() == MediaType.APPLICATION_JSON_TYPE - e.response.getBody(String.class).get().length() > 0 - } - - void "test DELETE method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.DELETE("/").header("Host", "h")) - - then: - response.status() == HttpStatus.OK - response.getBody(String.class).isEmpty() - } - - void "test POST method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.POST("/Andriy", null).header("Host", "h")) - - then: - response.status() == HttpStatus.CREATED - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == "Hello, Andriy\n" - } - - void "test PUT method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.PUT("/Andriy", null).header("Host", "h")) - - then: - response.status() == HttpStatus.OK - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == "Hello, Andriy!\n" - } - - void "test GET method with serialization"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/cactus").header("Host", "h")) - - then: - response.status == HttpStatus.OK - response.contentType.get() == MediaType.APPLICATION_JSON_TYPE - response.getBody(Cactus.class).get() == new Cactus("green", 1) - } - -} diff --git a/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java new file mode 100644 index 000000000..f8b073693 --- /dev/null +++ b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java @@ -0,0 +1,87 @@ +package io.micronaut.http.poja.sample; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.poja.sample.model.Cactus; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest +public class SimpleServerTest { + + @Inject + @Client("/") + HttpClient client; + + @Test + void testGetMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.GET("/").header("Host", "h")); + + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getContentType().get()); + assertEquals("Hello, Micronaut Without Netty!\n", response.getBody(String.class).get()); + } + + @Test + void testInvalidGetMethod() { + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.GET("/test/invalid").header("Host", "h")); + }); + + assertEquals(HttpStatus.NOT_FOUND, e.getStatus()); + assertEquals(MediaType.APPLICATION_JSON_TYPE, e.getResponse().getContentType().get()); + assertTrue(e.getResponse().getBody(String.class).get().length() > 0); + } + + @Test + void testDeleteMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.DELETE("/").header("Host", "h")); + + assertEquals(HttpStatus.OK, response.status()); + response.getBody(String.class).isEmpty(); + } + + @Test + void testPostMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.POST("/Andriy", null).header("Host", "h")); + + assertEquals(HttpStatus.CREATED, response.status()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getContentType().get()); + assertEquals("Hello, Andriy\n", response.getBody(String.class).get()); + } + + @Test + void testPutMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.PUT("/Andriy", null).header("Host", "h")); + + assertEquals(HttpStatus.OK, response.status()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getContentType().get()); + assertEquals("Hello, Andriy!\n", response.getBody(String.class).get()); + } + + @Test + void testGetMethodWithSerialization() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.GET("/cactus").header("Host", "h")); + + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals(MediaType.APPLICATION_JSON_TYPE, response.getContentType().get()); + assertEquals(new Cactus("green", 1), response.getBody(Cactus.class).get()); ; + } + +}