From b4d688a679cbdd9b1b9cdb9c7f28d4f61b9b75fc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 11:54:39 +0200 Subject: [PATCH] Remove poja from 4.10.x branch (#782) * remove poja modules * core 4.6.2 * test 4.5.0 * security 4.10.0 * logging 1.4.0 * session 4.4.0 --- gradle/libs.versions.toml | 10 +- http-poja-apache/build.gradle | 40 -- .../llhttp/ApacheServerlessApplication.java | 110 ----- .../llhttp/ApacheServletConfiguration.java | 39 -- .../poja/llhttp/ApacheServletHttpRequest.java | 315 ------------ .../llhttp/ApacheServletHttpResponse.java | 145 ------ .../ApacheServletBadRequestException.java | 38 -- .../io.micronaut.http.HttpResponseFactory | 1 - .../poja/BaseServerlessApplicationSpec.groovy | 104 ---- .../http/poja/SimpleServerSpec.groovy | 147 ------ .../src/test/resources/logback.xml | 16 - http-poja-common/build.gradle | 34 -- .../http/poja/PojaBinderRegistry.java | 55 --- .../micronaut/http/poja/PojaBodyBinder.java | 269 ---------- .../micronaut/http/poja/PojaHttpRequest.java | 202 -------- .../micronaut/http/poja/PojaHttpResponse.java | 31 -- .../poja/PojaHttpServerlessApplication.java | 154 ------ ...rvlerlessApplicationContextConfigurer.java | 36 -- .../http/poja/util/LimitingInputStream.java | 88 ---- .../http/poja/util/MultiValueHeaders.java | 123 ----- .../poja/util/MultiValuesQueryParameters.java | 91 ---- .../http/poja/util/QueryStringDecoder.java | 418 ---------------- .../io.micronaut.http.HttpResponseFactory | 1 - .../poja/util/LimitingInputStreamSpec.groovy | 39 -- .../poja/util/QueryStringDecoderTest.groovy | 465 ------------------ .../src/test/resources/logback.xml | 16 - http-poja-test/build.gradle | 33 -- .../TestingServerlessEmbeddedApplication.java | 295 ----------- .../http/poja/test/SimpleServerSpec.groovy | 118 ----- settings.gradle | 6 +- test-sample-poja/README.md | 39 -- test-sample-poja/build.gradle | 45 -- .../http/poja/sample/Application.java | 33 -- .../http/poja/sample/TestController.java | 60 --- .../http/poja/sample/SimpleServerSpec.groovy | 71 --- .../build.gradle | 9 - .../tck/poja/PojaApacheServerTestSuite.java | 40 -- .../tck/poja/PojaApacheServerUnderTest.java | 103 ---- .../PojaApacheServerUnderTestProvider.java | 30 -- .../reflect-config.json | 29 -- .../resource-config.json | 8 - ...micronaut.http.tck.ServerUnderTestProvider | 1 - 42 files changed, 6 insertions(+), 3901 deletions(-) delete mode 100644 http-poja-apache/build.gradle delete mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java delete mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java delete mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java delete mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java delete mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java delete mode 100644 http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory delete mode 100644 http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy delete mode 100644 http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy delete mode 100644 http-poja-apache/src/test/resources/logback.xml delete mode 100644 http-poja-common/build.gradle delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServlerlessApplicationContextConfigurer.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValueHeaders.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValuesQueryParameters.java delete mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java delete mode 100644 http-poja-common/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory delete mode 100644 http-poja-common/src/test/groovy/io/micronaut/http/poja/util/LimitingInputStreamSpec.groovy delete mode 100644 http-poja-common/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy delete mode 100644 http-poja-common/src/test/resources/logback.xml delete mode 100644 http-poja-test/build.gradle delete mode 100644 http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java delete mode 100644 http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy delete mode 100644 test-sample-poja/README.md delete mode 100644 test-sample-poja/build.gradle delete mode 100644 test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java delete mode 100644 test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java delete mode 100644 test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy delete mode 100644 test-suite-http-server-tck-poja-apache/build.gradle delete mode 100644 test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java delete mode 100644 test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java delete mode 100644 test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java delete mode 100644 test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json delete mode 100644 test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json delete mode 100644 test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1eb31cbb3..0dc08fec5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -micronaut = "4.6.1" +micronaut = "4.6.2" micronaut-docs = "2.0.0" -micronaut-test = "4.4.0" +micronaut-test = "4.5.0" groovy = "4.0.15" spock = "2.3-groovy-4.0" @@ -16,13 +16,13 @@ managed-apache-http-core5 = "5.2.5" managed-jetty = '11.0.22' micronaut-reactor = "3.5.0" -micronaut-security = "4.9.1" +micronaut-security = "4.10.0" micronaut-serde = "2.11.0" -micronaut-session = "4.3.0" +micronaut-session = "4.4.0" micronaut-validation = "4.7.0" google-cloud-functions = '1.1.0' kotlin = "1.9.25" -micronaut-logging = "1.3.0" +micronaut-logging = "1.4.0" # Micronaut micronaut-gradle-plugin = "4.4.2" diff --git a/http-poja-apache/build.gradle b/http-poja-apache/build.gradle deleted file mode 100644 index 4ee8b902f..000000000 --- a/http-poja-apache/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -plugins { - id("io.micronaut.build.internal.servlet.module") - id("idea") -} - -dependencies { - api(projects.micronautHttpPojaCommon) - implementation(libs.apache.http.core5) - - compileOnly(mn.reactor) - compileOnly(mn.micronaut.json.core) - - - testImplementation(mnSerde.micronaut.serde.jackson) -} - -micronautBuild { - binaryCompatibility { - enabled.set(false) - } -} - -javadoc { - failOnError(false) -} 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 deleted file mode 100644 index 4afaf6a8b..000000000 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.llhttp; - -import io.micronaut.context.ApplicationContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.MediaType; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.http.poja.PojaHttpServerlessApplication; -import io.micronaut.http.poja.llhttp.exception.ApacheServletBadRequestException; -import io.micronaut.http.server.exceptions.HttpServerException; -import io.micronaut.inject.qualifiers.Qualifiers; -import io.micronaut.runtime.ApplicationConfiguration; -import io.micronaut.scheduling.TaskExecutors; -import io.micronaut.servlet.http.ServletHttpHandler; -import jakarta.inject.Singleton; -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.impl.io.DefaultHttpResponseWriter; -import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl; -import org.apache.hc.core5.http.io.SessionOutputBuffer; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.ExecutorService; - -/** - * Implementation of {@link PojaHttpServerlessApplication} for Apache. - * - * @author Andriy Dmytruk. - * @since 4.10.0 - */ -@Singleton -public class ApacheServerlessApplication - extends PojaHttpServerlessApplication, ApacheServletHttpResponse> { - - private final ConversionService conversionService; - private final MediaTypeCodecRegistry codecRegistry; - private final ExecutorService ioExecutor; - private final ApacheServletConfiguration configuration; - - /** - * Default constructor. - * - * @param applicationContext The application context - * @param applicationConfiguration The application configuration - */ - public ApacheServerlessApplication(ApplicationContext applicationContext, - ApplicationConfiguration applicationConfiguration) { - super(applicationContext, applicationConfiguration); - conversionService = applicationContext.getConversionService(); - codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); - ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); - configuration = applicationContext.getBean(ApacheServletConfiguration.class); - } - - @Override - protected void handleSingleRequest( - ServletHttpHandler, ApacheServletHttpResponse> servletHttpHandler, - InputStream in, - OutputStream out - ) throws IOException { - ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); - try { - ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, response, configuration - ); - servletHttpHandler.service(exchange); - } catch (ApacheServletBadRequestException e) { - response.status(HttpStatus.BAD_REQUEST); - response.contentType(MediaType.TEXT_PLAIN_TYPE); - response.getOutputStream().write(e.getMessage().getBytes()); - } - writeResponse(response.getNativeResponse(), out); - } - - private void writeResponse(ClassicHttpResponse response, OutputStream out) throws IOException { - SessionOutputBuffer buffer = new SessionOutputBufferImpl(configuration.outputBufferSize()); - DefaultHttpResponseWriter responseWriter = new DefaultHttpResponseWriter(); - try { - responseWriter.write(response, buffer, out); - } catch (HttpException e) { - throw new HttpServerException("Could not write response body", e); - } - buffer.flush(out); - - HttpEntity entity = response.getEntity(); - if (entity != null) { - entity.writeTo(out); - } - out.flush(); - } - -} diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java deleted file mode 100644 index 7740bde64..000000000 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.llhttp; - -import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.core.bind.annotation.Bindable; - -/** - * Configuration specific to the Apache POJA serverless application. - * - * @param inputBufferSize The size of the buffer that is used to read and parse the HTTP request - * (in bytes). Default value is 8192 (8Kb). - * @param outputBufferSize The size of the buffer that is used to write the HTTP response - * (in bytes). Default value is 8192 (8Kb). - * @author Andriy Dmytruk - * @since 4.10.0 - */ -@ConfigurationProperties("poja.apache") -public record ApacheServletConfiguration( - @Bindable(defaultValue = "8192") - int inputBufferSize, - @Bindable(defaultValue = "8192") - int outputBufferSize -) { - -} 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 deleted file mode 100644 index d4683758e..000000000 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.llhttp; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionService; -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.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.DefaultHttpRequestParser; -import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; -import org.apache.hc.core5.net.URIBuilder; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalLong; -import java.util.concurrent.ExecutorService; -import java.util.stream.Collectors; - -/** - * An implementation of the POJA Http Request based on Apache. - * - * @param Body type - * @author Andriy Dmytruk - * @since 4.10.0 - */ -@Internal -public final class ApacheServletHttpRequest extends PojaHttpRequest { - - private final ClassicHttpRequest request; - - private final HttpMethod method; - private URI uri; - private final MultiValueHeaders headers; - private final MultiValuesQueryParameters queryParameters; - private final SimpleCookies cookies; - - private final ByteBody byteBody; - - /** - * Create an Apache-based request. - * - * @param inputStream The input stream - * @param conversionService The conversion service - * @param codecRegistry The media codec registry - * @param ioExecutor The executor service - * @param response The response - * @param configuration The configuration - */ - public ApacheServletHttpRequest( - InputStream inputStream, - ConversionService conversionService, - MediaTypeCodecRegistry codecRegistry, - ExecutorService ioExecutor, - ApacheServletHttpResponse response, - ApacheServletConfiguration configuration - ) { - super(conversionService, codecRegistry, response); - - SessionInputBufferImpl sessionInputBuffer - = new SessionInputBufferImpl(configuration.inputBufferSize()); - DefaultHttpRequestParser parser = new DefaultHttpRequestParser(); - - try { - request = parser.parse(sessionInputBuffer, inputStream); - } catch (HttpException | IOException e) { - throw new ApacheServletBadRequestException("HTTP request could not be parsed", e); - } - - method = HttpMethod.parse(request.getMethod()); - try { - uri = request.getUri(); - } catch (URISyntaxException e) { - throw new ApacheServletBadRequestException("Could not get request URI", e); - } - headers = createHeaders(request.getHeaders(), conversionService); - queryParameters = parseQueryParameters(uri, conversionService); - cookies = parseCookies(request, conversionService); - - 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); - } - } - - @Override - public ClassicHttpRequest getNativeRequest() { - return request; - } - - @Override - public @NonNull Cookies getCookies() { - return cookies; - } - - @Override - public @NonNull MutableHttpParameters getParameters() { - return queryParameters; - } - - @Override - public @NonNull HttpMethod getMethod() { - return method; - } - - @Override - public @NonNull URI getUri() { - return uri; - } - - @Override - public MutableHttpRequest cookie(Cookie cookie) { - cookies.put(cookie.getName(), cookie); - return this; - } - - @Override - public MutableHttpRequest uri(URI uri) { - this.uri = uri; - return this; - } - - @Override - public MutableHttpRequest body(T body) { - throw new UnsupportedOperationException("Could not change request body"); - } - - @Override - public @NonNull MutableHttpHeaders getHeaders() { - return headers; - } - - @Override - @SuppressWarnings("unchecked") - public @NonNull Optional getBody() { - return (Optional) getBody(Object.class); - } - - @Override - public @NonNull ByteBody byteBody() { - return byteBody; - } - - @Override - public void setConversionService(@NonNull ConversionService conversionService) { - // Not implemented - } - - private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { - SimpleCookies cookies = new SimpleCookies(conversionService); - - // Manually parse cookies from the response headers - for (Header header : request.getHeaders(HttpHeaders.COOKIE)) { - String cookie = header.getValue(); - - String name = null; - int start = 0; - for (int i = 0; i < cookie.length(); ++i) { - if (i < cookie.length() - 1 && cookie.charAt(i) == ';' && cookie.charAt(i + 1) == ' ') { - if (name != null) { - cookies.put(name, Cookie.of(name, cookie.substring(start, i))); - name = null; - start = i + 2; - ++i; - } - } else if (cookie.charAt(i) == '=') { - name = cookie.substring(start, i); - start = i + 1; - } - } - if (name != null) { - cookies.put(name, Cookie.of(name, cookie.substring(start))); - } - } - return cookies; - } - - private static MultiValueHeaders createHeaders( - Header[] headers, ConversionService conversionService - ) { - Map> map = new LinkedHashMap<>(); - for (Header header: headers) { - if (!map.containsKey(header.getName())) { - map.put(header.getName(), new ArrayList<>(1)); - } - map.get(header.getName()).add(header.getValue()); - } - return new MultiValueHeaders(map, conversionService); - } - - private static MultiValuesQueryParameters parseQueryParameters(URI uri, ConversionService conversionService) { - Map> map = new URIBuilder(uri).getQueryParams().stream() - .collect(Collectors.groupingBy( - NameValuePair::getName, - Collectors.mapping(NameValuePair::getValue, Collectors.toList()) - )); - return new MultiValuesQueryParameters(map, conversionService); - } - - /** - * An input stream that would initially delegate to the first input stream - * and then to the second one. Created specifically to be used with {@link ByteBody}. - */ - private static final class CombinedInputStream extends InputStream { - - private final InputStream first; - private final InputStream second; - private boolean finishedFirst; - - /** - * Create the input stream from first stream and second stream. - * - * @param first The first stream - * @param second The second stream - */ - CombinedInputStream(InputStream first, InputStream second) { - this.first = first; - this.second = second; - } - - @Override - public int read() throws IOException { - if (finishedFirst) { - return second.read(); - } - int result = first.read(); - if (result == -1) { - finishedFirst = true; - return second.read(); - } - return result; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (finishedFirst) { - return second.read(b, off, len); - } - int readLength = first.read(b, off, len); - if (readLength < len) { - finishedFirst = true; - readLength += second.read(b, off + readLength, len - readLength); - } - return readLength; - } - - @Override - public void close() throws IOException { - first.close(); - second.close(); - } - } - -} diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java deleted file mode 100644 index 3f837cc9b..000000000 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.llhttp; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.convert.value.MutableConvertibleValuesMap; -import io.micronaut.http.HttpHeaders; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.MutableHttpHeaders; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.http.poja.PojaHttpResponse; -import io.micronaut.http.simple.SimpleHttpHeaders; -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.io.entity.ByteArrayEntity; -import org.apache.hc.core5.http.message.BasicClassicHttpResponse; - -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.Optional; - -/** - * An implementation of the POJA HTTP response based on Apache. - * - * @param The body type - * @author Andriy Dmytruk - * @since 4.10.0 - */ -@Internal -public final class ApacheServletHttpResponse extends PojaHttpResponse { - - private int code = HttpStatus.OK.getCode(); - private String reasonPhrase = HttpStatus.OK.getReason(); - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - private final SimpleHttpHeaders headers; - private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); - private T bodyObject; - - /** - * Create an Apache-based response. - * - * @param conversionService The conversion service - */ - public ApacheServletHttpResponse(ConversionService conversionService) { - this.headers = new SimpleHttpHeaders(conversionService); - } - - @Override - public ClassicHttpResponse getNativeResponse() { - headers.remove(HttpHeaders.CONTENT_LENGTH); - headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size())); - if ("chunked".equalsIgnoreCase(headers.get(HttpHeaders.TRANSFER_ENCODING))) { - headers.remove(HttpHeaders.TRANSFER_ENCODING); - } - - BasicClassicHttpResponse response = new BasicClassicHttpResponse(code, reasonPhrase); - headers.forEachValue(response::addHeader); - ContentType contentType = headers.getContentType().map(ContentType::parse) - .orElse(ContentType.APPLICATION_JSON); - ByteArrayEntity body = new ByteArrayEntity(out.toByteArray(), contentType); - response.setEntity(body); - return response; - } - - @Override - public OutputStream getOutputStream() throws IOException { - return out; - } - - @Override - public BufferedWriter getWriter() throws IOException { - return new BufferedWriter(new PrintWriter(out)); - } - - @Override - public MutableHttpResponse cookie(Cookie cookie) { - return this; - } - - @Override - public MutableHttpResponse body(@Nullable B body) { - this.bodyObject = (T) body; - return (MutableHttpResponse) this; - } - - @NonNull - @Override - public Optional getBody() { - return Optional.ofNullable(bodyObject); - } - - @Override - public MutableHttpResponse status(int code, CharSequence message) { - this.code = code; - if (message == null) { - this.reasonPhrase = HttpStatus.getDefaultReason(code); - } else { - this.reasonPhrase = message.toString(); - } - return this; - } - - @Override - public int code() { - return code; - } - - @Override - public String reason() { - return reasonPhrase; - } - - @Override - public MutableHttpHeaders getHeaders() { - return headers; - } - - @Override - public @NonNull MutableConvertibleValues getAttributes() { - return attributes; - } - -} diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java deleted file mode 100644 index 3efb664eb..000000000 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.llhttp.exception; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.http.server.exceptions.HttpServerException; - -/** - * An exception that gets thrown in case of a bad request sent by user. - * The exception is specific to Apache request parsing. - */ -@Internal -public final class ApacheServletBadRequestException extends HttpServerException { - - /** - * Create an apache bad request exception. - * - * @param message The message to send to user - * @param cause The cause - */ - public ApacheServletBadRequestException(String message, Exception cause) { - super(message, cause); - } - -} diff --git a/http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory deleted file mode 100644 index 932eae367..000000000 --- a/http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.servlet.http.ServletResponseFactory \ No newline at end of file diff --git a/http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy deleted file mode 100644 index 1a3964558..000000000 --- a/http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy +++ /dev/null @@ -1,104 +0,0 @@ -package io.micronaut.http.poja - -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Replaces -import io.micronaut.http.poja.llhttp.ApacheServerlessApplication -import io.micronaut.runtime.ApplicationConfiguration -import jakarta.inject.Inject -import jakarta.inject.Singleton -import spock.lang.Specification - -import java.nio.ByteBuffer -import java.nio.channels.Channels -import java.nio.channels.ClosedByInterruptException -import java.nio.channels.Pipe -import java.nio.charset.StandardCharsets -/** - * A base class for serverless application test - */ -abstract class BaseServerlessApplicationSpec extends Specification { - - @Inject - TestingServerlessApplication app - - /** - * An extension of {@link ApacheServerlessApplication} that creates 2 - * pipes to communicate with the server and simplifies reading and writing to them. - */ - @Singleton - @Replaces(ApacheServerlessApplication.class) - static class TestingServerlessApplication extends ApacheServerlessApplication { - - OutputStream input - Pipe.SourceChannel output - StringBuffer readInfo = new StringBuffer() - int lastIndex = 0 - - /** - * Default constructor. - * - * @param applicationContext The application context - * @param applicationConfiguration The application configuration - */ - TestingServerlessApplication(ApplicationContext applicationContext, ApplicationConfiguration applicationConfiguration) { - super(applicationContext, applicationConfiguration) - } - - @Override - ApacheServerlessApplication start() { - var inputPipe = Pipe.open() - var outputPipe = Pipe.open() - input = Channels.newOutputStream(inputPipe.sink()) - output = outputPipe.source() - - // Run the request handling on a new thread - new Thread(() -> { - start( - Channels.newInputStream(inputPipe.source()), - Channels.newOutputStream(outputPipe.sink()) - ) - }).start() - - // Run the reader thread - new Thread(() -> { - ByteBuffer buffer = ByteBuffer.allocate(1024) - try { - while (true) { - buffer.clear() - int bytes = output.read(buffer) - if (bytes == -1) { - break - } - buffer.flip() - - Character character - while (buffer.hasRemaining()) { - character = (char) buffer.get() - readInfo.append(character) - } - } - } catch (ClosedByInterruptException ignored) { - } - }).start() - - return this - } - - void write(String content) { - input.write(content.getBytes(StandardCharsets.UTF_8)) - } - - String read(int waitMillis = 300) { - // Wait the given amount of time. The approach needs to be improved - Thread.sleep(waitMillis) - - var result = readInfo.toString().substring(lastIndex) - lastIndex += result.length() - - return result - .replace('\r', '') - .replaceAll("Date: .*\n", "") - } - } - -} diff --git a/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy deleted file mode 100644 index b2da6492b..000000000 --- a/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy +++ /dev/null @@ -1,147 +0,0 @@ -package io.micronaut.http.poja - - -import io.micronaut.core.annotation.NonNull -import io.micronaut.http.HttpStatus -import io.micronaut.http.MediaType -import io.micronaut.http.annotation.* -import io.micronaut.test.extensions.spock.annotation.MicronautTest - -@MicronautTest -class SimpleServerSpec extends BaseServerlessApplicationSpec { - - void "test GET method"() { - when: - app.write("""\ - GET /test HTTP/1.1 - Host: h - - """.stripIndent()) - - then: - app.read() == """\ - HTTP/1.1 200 Ok - Content-Length: 32 - Content-Type: text/plain - - Hello, Micronaut Without Netty! - """.stripIndent() - } - - void "test invalid GET method"() { - when: - app.write("""\ - GET /invalid-test HTTP/1.1 - Host: h - - """.stripIndent()) - - then: - app.read() == """\ - HTTP/1.1 404 Not Found - Content-Length: 140 - Content-Type: application/json - - {"_links":{"self":[{"href":"/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".stripIndent() - } - - void "test non-parseable GET method"() { - when: - app.write("""\ - GET /test HTTP/1.1error - Host: h - - """.stripIndent()) - - then: - app.read() == """\ - HTTP/1.1 400 Bad Request - Content-Length: 32 - Content-Type: text/plain - - HTTP request could not be parsed""".stripIndent() - } - - void "test DELETE method"() { - when: - app.write("""\ - DELETE /test HTTP/1.1 - Host: h - - """.stripIndent()) - - then: - app.read() == """\ - HTTP/1.1 200 Ok - Content-Length: 0 - - """.stripIndent() - } - - void "test POST method"() { - when: - app.write("""\ - POST /test/Dream HTTP/1.1 - Host: h - - """.stripIndent()) - - then: - app.read() == """\ - HTTP/1.1 201 Created - Content-Length: 13 - Content-Type: text/plain - - Hello, Dream - """.stripIndent() - } - - void "test PUT method"() { - when: - app.write("""\ - PUT /test/Dream1 HTTP/1.1 - Host: h - - """.stripIndent()) - - then: - app.read() == """\ - HTTP/1.1 200 Ok - Content-Length: 15 - Content-Type: text/plain - - Hello, Dream1! - """.stripIndent() - } - - /** - * A controller for testing. - */ - @Controller(value = "/test", produces = MediaType.TEXT_PLAIN, consumes = MediaType.ALL) - static class TestController { - - @Get - String index() { - return "Hello, Micronaut Without Netty!\n" - } - - @Delete - void delete() { - System.err.println("Delete called") - } - - @Post("/{name}") - @Status(HttpStatus.CREATED) - String create(@NonNull String name) { - return "Hello, " + name + "\n" - } - - @Put("/{name}") - @Status(HttpStatus.OK) - String update(@NonNull String name) { - return "Hello, " + name + "!\n" - } - - } - -} diff --git a/http-poja-apache/src/test/resources/logback.xml b/http-poja-apache/src/test/resources/logback.xml deleted file mode 100644 index ef2b2f918..000000000 --- a/http-poja-apache/src/test/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - System.err - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - diff --git a/http-poja-common/build.gradle b/http-poja-common/build.gradle deleted file mode 100644 index 27f4afdd2..000000000 --- a/http-poja-common/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -plugins { - id("io.micronaut.build.internal.servlet.module") -} - -dependencies { - api(projects.micronautServletCore) - - compileOnly(mn.reactor) - compileOnly(mn.micronaut.json.core) - - testImplementation(mn.reactor) - testImplementation(mnSerde.micronaut.serde.jackson) -} - -micronautBuild { - binaryCompatibility { - enabled.set(false) - } -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java deleted file mode 100644 index 077b3a3ec..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja; - -import io.micronaut.context.annotation.Replaces; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.bind.DefaultRequestBinderRegistry; -import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder; -import io.micronaut.http.bind.binders.RequestArgumentBinder; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.servlet.http.ServletBinderRegistry; -import jakarta.inject.Singleton; - -import java.util.List; - -/** - * An argument binder registry implementation for serverless POJA applications. - */ -@Internal -@Singleton -@Replaces(DefaultRequestBinderRegistry.class) -class PojaBinderRegistry extends ServletBinderRegistry { - - /** - * Default constructor. - * @param mediaTypeCodecRegistry The media type codec registry - * @param conversionService The conversion service - * @param binders Any registered binders - * @param defaultBodyAnnotationBinder The default binder - */ - public PojaBinderRegistry(MediaTypeCodecRegistry mediaTypeCodecRegistry, - ConversionService conversionService, - List binders, - DefaultBodyAnnotationBinder defaultBodyAnnotationBinder - ) { - super(mediaTypeCodecRegistry, conversionService, binders, defaultBodyAnnotationBinder); - - this.byAnnotation.put(Body.class, new PojaBodyBinder<>(conversionService, mediaTypeCodecRegistry, defaultBodyAnnotationBinder)); - } -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java deleted file mode 100644 index eadd92a25..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionError; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.io.IOUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MediaType; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; -import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder; -import io.micronaut.http.codec.CodecException; -import io.micronaut.http.codec.MediaTypeCodec; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.json.codec.MapperMediaTypeCodec; -import io.micronaut.json.tree.JsonNode; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Array; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -/** - * A body binder implementation for serverless POJA applications. - * - * @param The body type - */ -@Internal -final class PojaBodyBinder implements AnnotatedRequestArgumentBinder { - private static final Logger LOG = LoggerFactory.getLogger(PojaBodyBinder.class); - private final MediaTypeCodecRegistry mediaTypeCodeRegistry; - private final DefaultBodyAnnotationBinder defaultBodyBinder; - private final ConversionService conversionService; - - /** - * Default constructor. - * - * @param conversionService The conversion service - * @param mediaTypeCodecRegistry The codec registry - */ - protected PojaBodyBinder( - ConversionService conversionService, - MediaTypeCodecRegistry mediaTypeCodecRegistry, - DefaultBodyAnnotationBinder defaultBodyAnnotationBinder) { - this.defaultBodyBinder = defaultBodyAnnotationBinder; - this.mediaTypeCodeRegistry = mediaTypeCodecRegistry; - this.conversionService = conversionService; - } - - @Override - public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { - final Argument argument = context.getArgument(); - final Class type = argument.getType(); - String name = argument.getAnnotationMetadata().stringValue(Body.class).orElse(null); - - if (source instanceof PojaHttpRequest pojaHttpRequest) { - if (CharSequence.class.isAssignableFrom(type) && name == null) { - return (BindingResult) bindCharSequence(pojaHttpRequest, source); - } else if (argument.getType().isAssignableFrom(byte[].class) && name == null) { - return (BindingResult) bindByteArray(pojaHttpRequest); - } else { - final MediaType mediaType = source.getContentType().orElse(MediaType.APPLICATION_JSON_TYPE); - if (pojaHttpRequest.isFormSubmission()) { - return bindFormData(pojaHttpRequest, name, context); - } - - final MediaTypeCodec codec = mediaTypeCodeRegistry - .findCodec(mediaType, type) - .orElse(null); - if (codec != null) { - return bindWithCodec(pojaHttpRequest, source, codec, argument, type, name); - } - } - } - LOG.trace("Not a function request, falling back to default body decoding"); - return defaultBodyBinder.bind(context, source); - } - - private BindingResult bindWithCodec( - PojaHttpRequest pojaHttpRequest, HttpRequest source, MediaTypeCodec codec, - Argument argument, Class type, String name - ) { - LOG.trace("Decoding function body with codec: {}", codec.getClass().getSimpleName()); - return pojaHttpRequest.consumeBody(inputStream -> { - try { - if (Publishers.isConvertibleToPublisher(type)) { - return bindPublisher(argument, type, codec, inputStream); - } else { - return bindPojo(argument, type, codec, inputStream, name); - } - } catch (CodecException e) { - LOG.trace("Error occurred decoding function body: {}", e.getMessage(), e); - return new ConversionFailedBindingResult<>(e); - } - }); - } - - private BindingResult bindCharSequence(PojaHttpRequest pojaHttpRequest, HttpRequest source) { - return pojaHttpRequest.consumeBody(inputStream -> { - try { - String content = IOUtils.readText(new BufferedReader(new InputStreamReader( - inputStream, source.getCharacterEncoding() - ))); - LOG.trace("Read content of length {} from function body", content.length()); - return () -> Optional.of(content); - } catch (IOException e) { - LOG.debug("Error occurred reading function body: {}", e.getMessage(), e); - return new ConversionFailedBindingResult<>(e); - } - }); - } - - private BindingResult bindByteArray(PojaHttpRequest pojaHttpRequest) { - return pojaHttpRequest.consumeBody(inputStream -> { - try { - byte[] bytes = inputStream.readAllBytes(); - return () -> Optional.of(bytes); - } catch (IOException e) { - LOG.debug("Error occurred reading function body: {}", e.getMessage(), e); - return new ConversionFailedBindingResult<>(e); - } - }); - } - - private BindingResult bindFormData( - PojaHttpRequest servletHttpRequest, String name, ArgumentConversionContext context - ) { - Optional form = servletHttpRequest.getBody(PojaHttpRequest.CONVERTIBLE_VALUES_ARGUMENT); - if (form.isEmpty()) { - return BindingResult.empty(); - } - if (name != null) { - return () -> form.get().get(name, context); - } - return () -> conversionService.convert(form.get().asMap(), context); - } - - private BindingResult bindPojo( - Argument argument, Class type, MediaTypeCodec codec, InputStream inputStream, String name - ) { - Argument requiredArg = type.isArray() ? Argument.listOf(type.getComponentType()) : argument; - Object converted; - - if (name != null && codec instanceof MapperMediaTypeCodec jsonCodec) { - // Special case where a particular part of body is required - try { - JsonNode node = jsonCodec.getJsonMapper() - .readValue(inputStream, JsonNode.class); - JsonNode field = node.get(name); - if (field == null) { - return Optional::empty; - } - converted = jsonCodec.decode(requiredArg, field); - } catch (IOException e) { - throw new CodecException("Error decoding JSON stream for type [JsonNode]: " + e.getMessage(), e); - } - } else { - converted = codec.decode(argument, inputStream); - } - - if (type.isArray()) { - converted = ((List) converted).toArray((Object[]) Array.newInstance(type.getComponentType(), 0)); - } - T content = (T) converted; - LOG.trace("Decoded object from function body: {}", converted); - return () -> Optional.of(content); - } - - private BindingResult bindPublisher( - Argument argument, Class type, MediaTypeCodec codec, InputStream inputStream - ) { - final Argument typeArg = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); - if (Publishers.isSingle(type)) { - T content = (T) codec.decode(typeArg, inputStream); - final Publisher publisher = Publishers.just(content); - LOG.trace("Decoded single publisher from function body: {}", content); - final T converted = conversionService.convertRequired(publisher, type); - return () -> Optional.of(converted); - } else { - final Argument> containerType = Argument.listOf(typeArg.getType()); - if (codec instanceof MapperMediaTypeCodec jsonCodec) { - // Special JSON case: we can accept both array and a single value - try { - JsonNode node = jsonCodec.getJsonMapper() - .readValue(inputStream, JsonNode.class); - T converted; - if (node.isArray()) { - converted = Publishers.convertPublisher( - conversionService, - Flux.fromIterable(node.values()) - .map(itemNode -> jsonCodec.decode(typeArg, itemNode)), - type - ); - } else { - converted = Publishers.convertPublisher( - conversionService, - Mono.just(jsonCodec.decode(typeArg, node)), - type - ); - } - return () -> Optional.of(converted); - } catch (IOException e) { - throw new CodecException("Error decoding JSON stream for type [JsonNode]: " + e.getMessage(), e); - } - } - T content = (T) codec.decode(containerType, inputStream); - LOG.trace("Decoded flux publisher from function body: {}", content); - final Flux flowable = Flux.fromIterable((Iterable) content); - final T converted = conversionService.convertRequired(flowable, type); - return () -> Optional.of(converted); - } - } - - @Override - public Class getAnnotationType() { - return Body.class; - } - - /** - * A binding result implementation for the case when conversion error was thrown. - * - * @param The type to be bound - * @param e The conversion error - */ - private record ConversionFailedBindingResult( - Exception e - ) implements BindingResult { - - @Override - public Optional getValue() { - return Optional.empty(); - } - - @Override - public List getConversionErrors() { - return Collections.singletonList(() -> e); - } - - } - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java deleted file mode 100644 index 4c5694878..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleMultiValues; -import io.micronaut.core.convert.value.ConvertibleMultiValuesMap; -import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.convert.value.MutableConvertibleValuesMap; -import io.micronaut.core.io.IOUtils; -import io.micronaut.core.type.Argument; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.MediaType; -import io.micronaut.http.MutableHttpRequest; -import io.micronaut.http.ServerHttpRequest; -import io.micronaut.http.body.ByteBody; -import io.micronaut.http.body.ByteBody.SplitBackpressureMode; -import io.micronaut.http.body.CloseableByteBody; -import io.micronaut.http.codec.MediaTypeCodec; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.http.poja.util.QueryStringDecoder; -import io.micronaut.servlet.http.ServletExchange; -import io.micronaut.servlet.http.ServletHttpRequest; -import io.micronaut.servlet.http.ServletHttpResponse; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.function.Function; - -/** - * A base class for serverless POJA requests that provides a number of common methods - * to be reused for body and binding. - * - * @param The body type - * @param The POJA request type - * @param The POJA response type - * @author Andriy - * @since 4.10.0 - */ -public abstract class PojaHttpRequest - implements ServletHttpRequest, ServerHttpRequest, ServletExchange, MutableHttpRequest { - - public static final Argument CONVERTIBLE_VALUES_ARGUMENT = Argument.of(ConvertibleValues.class); - - protected final ConversionService conversionService; - protected final MediaTypeCodecRegistry codecRegistry; - protected final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); - protected final PojaHttpResponse response; - - public PojaHttpRequest( - ConversionService conversionService, - MediaTypeCodecRegistry codecRegistry, - PojaHttpResponse response - ) { - this.conversionService = conversionService; - this.codecRegistry = codecRegistry; - this.response = response; - } - - @Override - public abstract ByteBody byteBody(); - - @Override - public @NonNull MutableConvertibleValues getAttributes() { - // Attributes are used for sharing internal data used by Micronaut logic. - // We need to store them and provide when needed. - return attributes; - } - - /** - * A utility method that allows consuming body. - * - * @return The result - * @param The function return value - * @param consumer The method to consume the body - */ - public T consumeBody(Function consumer) { - try (CloseableByteBody byteBody = byteBody().split(SplitBackpressureMode.FASTEST)) { - return consumer.apply(byteBody.toInputStream()); - } - } - - @Override - public @NonNull Optional getBody(@NonNull ArgumentConversionContext conversionContext) { - Argument arg = conversionContext.getArgument(); - if (arg == null) { - return Optional.empty(); - } - final Class type = arg.getType(); - final MediaType contentType = getContentType().orElse(MediaType.APPLICATION_JSON_TYPE); - - if (isFormSubmission()) { - ConvertibleMultiValues form = getFormData(); - if (ConvertibleValues.class == type || Object.class == type) { - return Optional.of((T) form); - } else { - return conversionService.convert(form.asMap(), arg); - } - } - - final MediaTypeCodec codec = codecRegistry.findCodec(contentType, type).orElse(null); - if (codec == null) { - return Optional.empty(); - } - if (ConvertibleValues.class == type || Object.class == type) { - final Map map = consumeBody(inputStream -> codec.decode(Map.class, inputStream)); - ConvertibleValues result = ConvertibleValues.of(map); - return Optional.of((T) result); - } else { - final T value = consumeBody(inputStream -> codec.decode(arg, inputStream)); - return Optional.of(value); - } - } - - /** - * A method used for retrieving form data. Can be overridden by specific implementations. - * - * @return The form data as multi-values. - */ - protected ConvertibleMultiValues getFormData() { - return consumeBody(inputStream -> { - try { - String content = IOUtils.readText(new BufferedReader(new InputStreamReader( - inputStream, getCharacterEncoding() - ))); - return parseFormData(content); - } catch (IOException e) { - throw new RuntimeException("Unable to parse body", e); - } - }); - } - - @Override - public InputStream getInputStream() { - return byteBody().split(SplitBackpressureMode.FASTEST).toInputStream(); - } - - @Override - public BufferedReader getReader() { - return new BufferedReader(new InputStreamReader(getInputStream())); - } - - /** - * Whether the request body is a form. - * - * @return Whether it is a form submission - */ - public boolean isFormSubmission() { - MediaType contentType = getContentType().orElse(null); - return MediaType.APPLICATION_FORM_URLENCODED_TYPE.equals(contentType) - || MediaType.MULTIPART_FORM_DATA_TYPE.equals(contentType); - } - - @Override - public ServletHttpRequest getRequest() { - return (ServletHttpRequest) this; - } - - @Override - public ServletHttpResponse getResponse() { - return response; - } - - private ConvertibleMultiValues parseFormData(String body) { - Map parameterValues = new QueryStringDecoder(body, false).parameters(); - - // Remove empty values - Iterator>> iterator = parameterValues.entrySet().iterator(); - while (iterator.hasNext()) { - List value = iterator.next().getValue(); - if (value.isEmpty() || StringUtils.isEmpty(value.get(0))) { - iterator.remove(); - } - } - - return new ConvertibleMultiValuesMap(parameterValues, conversionService); - } - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java deleted file mode 100644 index 8a319987d..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja; - -import io.micronaut.servlet.http.ServletHttpResponse; - -/** - * A base class for serverless POJA responses. - * - * @param The body type - * @param The POJA response type - * @author Andriy Dmytruk - * @since 4.10.0 - */ -public abstract class PojaHttpResponse implements ServletHttpResponse { - - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java deleted file mode 100644 index cb14fdc02..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja; - -import io.micronaut.context.ApplicationContext; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.runtime.ApplicationConfiguration; -import io.micronaut.runtime.EmbeddedApplication; -import io.micronaut.servlet.http.ServletExchange; -import io.micronaut.servlet.http.ServletHttpHandler; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.Channel; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; - -/** - * A base class for POJA serverless applications. - * It implements {@link EmbeddedApplication} for POSIX serverless environments. - * - * @param The request type - * @param The response type - * @author Andriy Dmytruk. - * @since 4.10.0 - */ -public abstract class PojaHttpServerlessApplication implements EmbeddedApplication> { - - private final ApplicationContext applicationContext; - private final ApplicationConfiguration applicationConfiguration; - - /** - * Default constructor. - * - * @param applicationContext The application context - * @param applicationConfiguration The application configuration - */ - public PojaHttpServerlessApplication(ApplicationContext applicationContext, - ApplicationConfiguration applicationConfiguration) { - this.applicationContext = applicationContext; - this.applicationConfiguration = applicationConfiguration; - } - - @Override - public ApplicationContext getApplicationContext() { - return applicationContext; - } - - @Override - public ApplicationConfiguration getApplicationConfiguration() { - return applicationConfiguration; - } - - @Override - public boolean isRunning() { - return true; // once this bean is instantiated, we assume it's running, so return true. - } - - /** - * Run the application using a particular channel. - * - * @param input The input stream - * @param output The output stream - * @return The application - */ - public @NonNull PojaHttpServerlessApplication start(InputStream input, OutputStream output) { - final ServletHttpHandler servletHttpHandler = - new ServletHttpHandler<>(applicationContext, null) { - @Override - protected ServletExchange createExchange(Object request, Object response) { - throw new UnsupportedOperationException("Not expected in serverless mode."); - } - }; - try { - runIndefinitely(servletHttpHandler, input, output); - } catch (IOException e) { - throw new RuntimeException(e); - } - return this; - } - - @Override - public @NonNull PojaHttpServerlessApplication start() { - try { - // Default streams to streams based on System.inheritedChannel. - // If not possible, use System.in/out. - Channel channel = System.inheritedChannel(); - if (channel != null) { - try (InputStream in = Channels.newInputStream((ReadableByteChannel) channel); - OutputStream out = Channels.newOutputStream((WritableByteChannel) channel)) { - return start(in, out); - } - } else { - return start(System.in, System.out); - } - } catch (IOException e) { - throw new RuntimeException(); - } - } - - /** - * A method to start the application in a loop. - * - * @param servletHttpHandler The handler - * @param in The input stream - * @param out The output stream - * @throws IOException IO exception - */ - @SuppressWarnings({"InfiniteLoopStatement", "java:S2189"}) - protected void runIndefinitely( - ServletHttpHandler servletHttpHandler, - InputStream in, - OutputStream out - ) throws IOException { - while (true) { - handleSingleRequest(servletHttpHandler, in, out); - } - } - - /** - * Handle a single request. - * - * @param servletHttpHandler The handler - * @param in The input stream - * @param out The output stream - * @throws IOException IO exception - */ - protected abstract void handleSingleRequest( - ServletHttpHandler servletHttpHandler, - InputStream in, - OutputStream out - ) throws IOException; - - @Override - public @NonNull PojaHttpServerlessApplication stop() { - return EmbeddedApplication.super.stop(); - } - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServlerlessApplicationContextConfigurer.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServlerlessApplicationContextConfigurer.java deleted file mode 100644 index 78eb60d77..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServlerlessApplicationContextConfigurer.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja; - -import io.micronaut.context.ApplicationContextBuilder; -import io.micronaut.context.ApplicationContextConfigurer; -import io.micronaut.context.annotation.ContextConfigurer; -import io.micronaut.core.annotation.NonNull; - -/** - * A class to configure application with POJA serverless specifics. - */ -@ContextConfigurer -public final class PojaHttpServlerlessApplicationContextConfigurer implements ApplicationContextConfigurer { - - @Override - public void configure(@NonNull ApplicationContextBuilder builder) { - // Need to disable banner because Micronaut prints banner to STDOUT, - // which gets mixed with HTTP response. See GCN-4489. - builder.banner(false); - } - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java b/http-poja-common/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java deleted file mode 100644 index c6a28ac04..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.util; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A wrapper around input stream that limits the maximum size to be read. - */ -public class LimitingInputStream extends InputStream { - - private long size; - private final InputStream stream; - private final long maxSize; - - /** - * Create the limiting input stream. - * - * @param stream The delegate stream - * @param maxSize The maximum size to read - */ - public LimitingInputStream(InputStream stream, long maxSize) { - this.maxSize = maxSize; - this.stream = stream; - } - - @Override - public int read() throws IOException { - if (size >= maxSize) { - return -1; - } - ++size; - return stream.read(); - } - - @Override - public int read(byte[] b) throws IOException { - synchronized (this) { - if (size >= maxSize) { - return -1; - } - if (b.length + size > maxSize) { - return read(b, 0, (int) (maxSize - size)); - } - int sizeRead = stream.read(b); - size += sizeRead; - return sizeRead; - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - synchronized (this) { - if (size >= maxSize) { - return -1; - } - int lengthToRead = (int) Math.min(len, maxSize - size); - int sizeRead = stream.read(b, off, lengthToRead); - size += sizeRead; - return sizeRead; - } - } - - @Override - public int available() throws IOException { - return (int) (maxSize - size); - } - - @Override - public void close() throws IOException { - stream.close(); - } - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValueHeaders.java b/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValueHeaders.java deleted file mode 100644 index 7e6d8cc02..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValueHeaders.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.util; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.MutableConvertibleMultiValuesMap; -import io.micronaut.http.MutableHttpHeaders; - -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -/** - * Headers implementation based on a multi-value map. - * The implementation performs the header's standardization. - * - * @param headers The values - */ -public record MultiValueHeaders( - MutableConvertibleMultiValuesMap headers -) implements MutableHttpHeaders { - - public MultiValueHeaders(Map> headers, ConversionService conversionService) { - this(standardizeHeaders(headers, conversionService)); - } - - @Override - public List getAll(CharSequence name) { - return headers.getAll(standardizeHeader(name)); - } - - @Override - public @Nullable String get(CharSequence name) { - return headers.get(standardizeHeader(name)); - } - - @Override - public Set names() { - return headers.names(); - } - - @Override - public Collection> values() { - return headers.values(); - } - - @Override - public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - return headers.get(standardizeHeader(name), conversionContext); - } - - @Override - public MutableHttpHeaders add(CharSequence header, CharSequence value) { - headers.add(standardizeHeader(header), value == null ? null : value.toString()); - return this; - } - - @Override - public MutableHttpHeaders remove(CharSequence header) { - headers.remove(standardizeHeader(header)); - return this; - } - - @Override - public void setConversionService(@NonNull ConversionService conversionService) { - this.headers.setConversionService(conversionService); - } - - private static MutableConvertibleMultiValuesMap standardizeHeaders( - Map> headers, ConversionService conversionService - ) { - MutableConvertibleMultiValuesMap map - = new MutableConvertibleMultiValuesMap<>(new LinkedHashMap<>(), conversionService); - for (String key: headers.keySet()) { - map.put(standardizeHeader(key), headers.get(key)); - } - return map; - } - - private static String standardizeHeader(CharSequence charSequence) { - String s; - if (charSequence == null) { - return null; - } else if (charSequence instanceof String) { - s = (String) charSequence; - } else { - s = charSequence.toString(); - } - - StringBuilder result = new StringBuilder(s.length()); - boolean upperCase = true; - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (upperCase && 'a' <= c && c <= 'z') { - c = (char) (c - 32); - } else if (!upperCase && 'A' <= c && c <= 'Z') { - c = (char) (c + 32); - } - result.append(c); - upperCase = c == '-'; - } - return result.toString(); - } -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValuesQueryParameters.java b/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValuesQueryParameters.java deleted file mode 100644 index c17252e3b..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValuesQueryParameters.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.util; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.MutableConvertibleMultiValuesMap; -import io.micronaut.http.MutableHttpParameters; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -/** - * Query parameters implementation. - * - * @param queryParams The values - */ -public record MultiValuesQueryParameters( - MutableConvertibleMultiValuesMap queryParams -) implements MutableHttpParameters { - - /** - * Construct the query parameters. - * - * @param parameters The parameters as a map. - * @param conversionService The conversion service. - */ - public MultiValuesQueryParameters( - Map> parameters, - ConversionService conversionService - ) { - this(new MutableConvertibleMultiValuesMap<>(parameters, conversionService)); - } - - @Override - public List getAll(CharSequence name) { - return queryParams.getAll(name); - } - - @Override - public @Nullable String get(CharSequence name) { - return queryParams.get(name); - } - - @Override - public Set names() { - return queryParams.names(); - } - - @Override - public Collection> values() { - return queryParams.values(); - } - - @Override - public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - return queryParams.get(name, conversionContext); - } - - @Override - public MutableHttpParameters add(CharSequence name, List values) { - for (CharSequence value: values) { - queryParams.add(name, value == null ? null : value.toString()); - } - return this; - } - - @Override - public void setConversionService(@NonNull ConversionService conversionService) { - queryParams.setConversionService(conversionService); - } - -} diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java b/http-poja-common/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java deleted file mode 100644 index 1564e6d3a..000000000 --- a/http-poja-common/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright 2017-2012 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.util; - -import io.micronaut.core.util.ArgumentUtils; -import io.micronaut.core.util.StringUtils; - -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Splits an HTTP query string into a path string and key-value parameter pairs. - * This decoder is for one time use only. Create a new instance for each URI: - *
- * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("/hello?recipient=world&x=1;y=2");
- * assert decoder.path().equals("/hello");
- * assert decoder.parameters().get("recipient").get(0).equals("world");
- * assert decoder.parameters().get("x").get(0).equals("1");
- * assert decoder.parameters().get("y").get(0).equals("2");
- * 
- * - * This decoder can also decode the content of an HTTP POST request whose - * content type is application/x-www-form-urlencoded: - *
- * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("recipient=world&x=1;y=2", false);
- * ...
- * 
- * - * HashDOS vulnerability fix - * - * As a workaround to the HashDOS vulnerability, the decoder - * limits the maximum number of decoded key-value parameter pairs, up to {@literal 1024} by - * default, and you can configure it when you construct the decoder by passing an additional - * integer parameter. - * - *

This is forked from Netty. See - * - * QueryStringDecoder.java - * . - *

- */ -@SuppressWarnings("java:S3776" /* Reduce cognitive complexity warning */) -public class QueryStringDecoder { - - private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - private static final int DEFAULT_MAX_PARAMS = 1024; - - private final Charset charset; - private final String uri; - private final int maxParams; - private final boolean semicolonIsNormalChar; - private int pathEndIdx; - private String path; - private Map> params; - - /** - * Creates a new decoder that decodes the specified URI. The decoder will - * assume that the query string is encoded in UTF-8. - */ - public QueryStringDecoder(String uri) { - this(uri, DEFAULT_CHARSET); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(String uri, boolean hasPath) { - this(uri, DEFAULT_CHARSET, hasPath); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(String uri, Charset charset) { - this(uri, charset, true); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(String uri, Charset charset, boolean hasPath) { - this(uri, charset, hasPath, DEFAULT_MAX_PARAMS); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) { - this(uri, charset, hasPath, maxParams, false); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(String uri, Charset charset, boolean hasPath, - int maxParams, boolean semicolonIsNormalChar) { - this.uri = ArgumentUtils.requireNonNull("uri", uri); - this.charset = ArgumentUtils.requireNonNull("charset", charset); - this.maxParams = ArgumentUtils.requirePositive("maxParams", maxParams); - this.semicolonIsNormalChar = semicolonIsNormalChar; - - // `-1` means that path end index will be initialized lazily - pathEndIdx = hasPath ? -1 : 0; - } - - /** - * Creates a new decoder that decodes the specified URI. The decoder will - * assume that the query string is encoded in UTF-8. - */ - public QueryStringDecoder(URI uri) { - this(uri, DEFAULT_CHARSET); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(URI uri, Charset charset) { - this(uri, charset, DEFAULT_MAX_PARAMS); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(URI uri, Charset charset, int maxParams) { - this(uri, charset, maxParams, false); - } - - /** - * Creates a new decoder that decodes the specified URI encoded in the - * specified charset. - */ - public QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) { - String rawPath = uri.getRawPath(); - if (rawPath == null) { - rawPath = StringUtils.EMPTY_STRING; - } - String rawQuery = uri.getRawQuery(); - // Also take care of cut of things like "http://localhost" - this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery; - this.charset = ArgumentUtils.requireNonNull("charset", charset); - this.maxParams = ArgumentUtils.requirePositive("maxParams", maxParams); - this.semicolonIsNormalChar = semicolonIsNormalChar; - pathEndIdx = rawPath.length(); - } - - @Override - public String toString() { - return uri(); - } - - /** - * Returns the uri used to initialize this {@link QueryStringDecoder}. - */ - public String uri() { - return uri; - } - - /** - * Returns the decoded path string of the URI. - */ - public String path() { - if (path == null) { - path = decodeComponent(uri, 0, pathEndIdx(), charset, true); - } - return path; - } - - /** - * Returns the decoded key-value parameter pairs of the URI. - */ - public Map> parameters() { - if (params == null) { - params = decodeParams(uri, pathEndIdx(), charset, maxParams, semicolonIsNormalChar); - } - return params; - } - - /** - * Returns the raw path string of the URI. - */ - public String rawPath() { - return uri.substring(0, pathEndIdx()); - } - - /** - * Returns raw query string of the URI. - */ - public String rawQuery() { - int start = pathEndIdx() + 1; - return start < uri.length() ? uri.substring(start) : StringUtils.EMPTY_STRING; - } - - private int pathEndIdx() { - if (pathEndIdx == -1) { - pathEndIdx = findPathEndIndex(uri); - } - return pathEndIdx; - } - - private static Map> decodeParams(String s, int from, Charset charset, int paramsLimit, - boolean semicolonIsNormalChar) { - int len = s.length(); - if (from >= len) { - return Collections.emptyMap(); - } - if (s.charAt(from) == '?') { - from++; - } - Map> params = new LinkedHashMap>(); - int nameStart = from; - int valueStart = -1; - int i; - loop: - for (i = from; i < len; i++) { - switch (s.charAt(i)) { - case '=': - if (nameStart == i) { - nameStart = i + 1; - } else if (valueStart < nameStart) { - valueStart = i + 1; - } - break; - case ';': - if (semicolonIsNormalChar) { - continue; - } - // fall-through - case '&': - if (addParam(s, nameStart, valueStart, i, params, charset)) { - paramsLimit--; - if (paramsLimit == 0) { - return params; - } - } - nameStart = i + 1; - break; - case '#': - break loop; - default: - // continue - } - } - addParam(s, nameStart, valueStart, i, params, charset); - return params; - } - - private static boolean addParam(String s, int nameStart, int valueStart, int valueEnd, - Map> params, Charset charset) { - if (nameStart >= valueEnd) { - return false; - } - if (valueStart <= nameStart) { - valueStart = valueEnd + 1; - } - String name = decodeComponent(s, nameStart, valueStart - 1, charset, false); - String value = decodeComponent(s, valueStart, valueEnd, charset, false); - List values = params.get(name); - if (values == null) { - values = new ArrayList(1); // Often there's only 1 value. - params.put(name, values); - } - values.add(value); - return true; - } - - /** - * Decodes a bit of a URL encoded by a browser. - *

- * This is equivalent to calling {@link #decodeComponent(String, Charset)} - * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2). - * @param s The string to decode (can be empty). - * @return The decoded string, or {@code s} if there's nothing to decode. - * If the string to decode is {@code null}, returns an empty string. - * @throws IllegalArgumentException if the string contains a malformed - * escape sequence. - */ - public static String decodeComponent(final String s) { - return decodeComponent(s, DEFAULT_CHARSET); - } - - /** - * Decodes a bit of a URL encoded by a browser. - *

- * The string is expected to be encoded as per RFC 3986, Section 2. - * This is the encoding used by JavaScript functions {@code encodeURI} - * and {@code encodeURIComponent}, but not {@code escape}. For example - * in this encoding, é (in Unicode {@code U+00E9} or in UTF-8 - * {@code 0xC3 0xA9}) is encoded as {@code %C3%A9} or {@code %c3%a9}. - *

- * This is essentially equivalent to calling - * {@link URLDecoder#decode(String, String)} - * except that it's over 2x faster and generates less garbage for the GC. - * Actually this function doesn't allocate any memory if there's nothing - * to decode, the argument itself is returned. - * @param s The string to decode (can be empty). - * @param charset The charset to use to decode the string (should really - * be {@link StandardCharsets#UTF_8}. - * @return The decoded string, or {@code s} if there's nothing to decode. - * If the string to decode is {@code null}, returns an empty string. - * @throws IllegalArgumentException if the string contains a malformed - * escape sequence. - */ - public static String decodeComponent(final String s, final Charset charset) { - if (s == null) { - return StringUtils.EMPTY_STRING; - } - return decodeComponent(s, 0, s.length(), charset, false); - } - - private static String decodeComponent(String s, int from, int toExcluded, Charset charset, boolean isPath) { - int len = toExcluded - from; - if (len <= 0) { - return StringUtils.EMPTY_STRING; - } - int firstEscaped = -1; - for (int i = from; i < toExcluded; i++) { - char c = s.charAt(i); - if (c == '%' || c == '+' && !isPath) { - firstEscaped = i; - break; - } - } - if (firstEscaped == -1) { - return s.substring(from, toExcluded); - } - - // Each encoded byte takes 3 characters (e.g. "%20") - int decodedCapacity = (toExcluded - firstEscaped) / 3; - byte[] buf = new byte[decodedCapacity]; - int bufIdx; - - StringBuilder strBuf = new StringBuilder(len); - strBuf.append(s, from, firstEscaped); - - for (int i = firstEscaped; i < toExcluded; i++) { - char c = s.charAt(i); - if (c != '%') { - strBuf.append(c != '+' || isPath? c : StringUtils.SPACE); - continue; - } - - bufIdx = 0; - do { - if (i + 3 > toExcluded) { - throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s); - } - buf[bufIdx++] = decodeHexByte(s, i + 1); - i += 3; - } while (i < toExcluded && s.charAt(i) == '%'); - i--; - - strBuf.append(new String(buf, 0, bufIdx, charset)); - } - return strBuf.toString(); - } - - private static int findPathEndIndex(String uri) { - int len = uri.length(); - for (int i = 0; i < len; i++) { - char c = uri.charAt(i); - if (c == '?' || c == '#') { - return i; - } - } - return len; - } - - private static int decodeHexNibble(char c) { - if ('0' <= c && c <= '9') { - return (char) (c - '0'); - } else if ('a' <= c && c <= 'f') { - return (char) (c - 'a' + 10); - } else if ('A' <= c && c <= 'F') { - return (char) (c - 'A' + 10); - } else { - return -1; - } - } - - private static byte decodeHexByte(CharSequence s, int pos) { - int hi = decodeHexNibble(s.charAt(pos)); - int lo = decodeHexNibble(s.charAt(pos + 1)); - if (hi != -1 && lo != -1) { - return (byte)((hi << 4) + lo); - } else { - throw new IllegalArgumentException(String.format("invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s)); - } - } - -} diff --git a/http-poja-common/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja-common/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory deleted file mode 100644 index 932eae367..000000000 --- a/http-poja-common/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.servlet.http.ServletResponseFactory \ No newline at end of file diff --git a/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/LimitingInputStreamSpec.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/LimitingInputStreamSpec.groovy deleted file mode 100644 index 2d0bb8a72..000000000 --- a/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/LimitingInputStreamSpec.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package io.micronaut.http.poja.util - -import io.micronaut.servlet.http.body.InputStreamByteBody -import spock.lang.Specification - -import java.util.concurrent.Executors - -class LimitingInputStreamSpec extends Specification { - - void "test LimitingInputStream"() { - when: - var stream = new ByteArrayInputStream("Hello world!".bytes) - var limiting = new LimitingInputStream(stream, 5) - - then: - new String(limiting.readAllBytes()) == "Hello" - } - - void "test LimitingInputStream with ByteBody"() { - when: - var stream = new ByteArrayInputStream("Hello world!".bytes) - var limiting = new LimitingInputStream(stream, 5) - var executor = Executors.newFixedThreadPool(1) - var body = InputStreamByteBody.create(limiting, OptionalLong.empty(), executor) - - then: - new String(body.toInputStream().readAllBytes()) == "Hello" - } - - void "test LimitingInputStream with larger limit"() { - when: - var stream = new ByteArrayInputStream("Hello".bytes) - var limiting = new LimitingInputStream(stream, 100) - - then: - new String(limiting.readAllBytes()) == "Hello" - } - -} diff --git a/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy deleted file mode 100644 index 53227b1ba..000000000 --- a/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright 2012 The Netty Project - * - * The Netty Project licenses this file to you under the Apache License, - * version 2.0 (the "License") you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package io.micronaut.http.poja.util - -import spock.lang.Specification - -import java.nio.charset.StandardCharsets -import java.util.Map.Entry -/** - * This is forked from Netty. See - * - * QueryStringDecoderTest.java - * . - */ -class QueryStringDecoderTest extends Specification { - - void testBasicUris() throws URISyntaxException { - when: - QueryStringDecoder d = new QueryStringDecoder(new URI("http://localhost/path")) - - then: - d.parameters().size() == 0 - } - - void testBasic() { - QueryStringDecoder d - - when: - d = new QueryStringDecoder("/foo") - - then: - d.path() == "/foo" - d.parameters().size() == 0 - - when: - d = new QueryStringDecoder("/foo%20bar") - - then: - d.path() == "/foo bar" - d.parameters().size() == 0 - - when: - d = new QueryStringDecoder("/foo?a=b=c") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("a").size() == 1 - d.parameters().get("a").get(0) == "b=c" - - when: - d = new QueryStringDecoder("/foo?a=1&a=2") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("a").size() == 2 - d.parameters().get("a").get(0) == "1" - d.parameters().get("a").get(1) == "2" - - when: - d = new QueryStringDecoder("/foo%20bar?a=1&a=2") - - then: - d.path() == "/foo bar" - d.parameters().size() == 1 - d.parameters().get("a").size() == 2 - d.parameters().get("a").get(0) == "1" - d.parameters().get("a").get(1) == "2" - - when: - d = new QueryStringDecoder("/foo?a=&a=2") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("a").size() == 2 - d.parameters().get("a").get(0) == "" - d.parameters().get("a").get(1) == "2" - - when: - d = new QueryStringDecoder("/foo?a=1&a=") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("a").size() == 2 - d.parameters().get("a").get(0) == "1" - d.parameters().get("a").get(1) == "" - - when: - d = new QueryStringDecoder("/foo?a=1&a=&a=") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("a").size() == 3 - d.parameters().get("a").get(0) == "1" - d.parameters().get("a").get(1) == "" - d.parameters().get("a").get(2) == "" - - when: - d = new QueryStringDecoder("/foo?a=1=&a==2") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("a").size() == 2 - d.parameters().get("a").get(0) == "1=" - d.parameters().get("a").get(1) == "=2" - - when: - d = new QueryStringDecoder("/foo?abc=1%2023&abc=124%20") - - then: - d.path() == "/foo" - d.parameters().size() == 1 - d.parameters().get("abc").size() == 2 - d.parameters().get("abc").get(0) == "1 23" - d.parameters().get("abc").get(1) == "124 " - - when: - d = new QueryStringDecoder("/foo?abc=%7E") - - then: - d.parameters().get("abc").get(0) == "~" - } - - void testExotic() { - expect: - assertQueryString("", "") - assertQueryString("foo", "foo") - assertQueryString("foo", "foo?") - assertQueryString("/foo", "/foo?") - assertQueryString("/foo", "/foo") - assertQueryString("?a=", "?a") - assertQueryString("foo?a=", "foo?a") - assertQueryString("/foo?a=", "/foo?a") - assertQueryString("/foo?a=", "/foo?a&") - assertQueryString("/foo?a=", "/foo?&a") - assertQueryString("/foo?a=", "/foo?&a&") - assertQueryString("/foo?a=", "/foo?&=a") - assertQueryString("/foo?a=", "/foo?=a&") - assertQueryString("/foo?a=", "/foo?a=&") - assertQueryString("/foo?a=b&c=d", "/foo?a=b&&c=d") - assertQueryString("/foo?a=b&c=d", "/foo?a=b&=&c=d") - assertQueryString("/foo?a=b&c=d", "/foo?a=b&==&c=d") - assertQueryString("/foo?a=b&c=&x=y", "/foo?a=b&c&x=y") - assertQueryString("/foo?a=", "/foo?a=") - assertQueryString("/foo?a=", "/foo?&a=") - assertQueryString("/foo?a=b&c=d", "/foo?a=b&c=d") - assertQueryString("/foo?a=1&a=&a=", "/foo?a=1&a&a=") - } - - void testSemicolon() { - expect: - assertQueryString("/foo?a=1;2", "/foo?a=1;2", false) - // "" should be treated as a normal character, see #8855 - assertQueryString("/foo?a=1;2", "/foo?a=1%3B2", true) - } - - void testPathSpecific() { - expect: - // decode escaped characters - new QueryStringDecoder("/foo%20bar/?").path() == "/foo bar/" - new QueryStringDecoder("/foo%0D%0A\\bar/?").path() == "/foo\r\n\\bar/" - - // a 'fragment' after '#' should be cuted (see RFC 3986) - new QueryStringDecoder("#123").path() == "" - new QueryStringDecoder("foo?bar#anchor").path() == "foo" - new QueryStringDecoder("/foo-bar#anchor").path() == "/foo-bar" - new QueryStringDecoder("/foo-bar#a#b?c=d").path() == "/foo-bar" - - // '+' is not escape ' ' for the path - new QueryStringDecoder("+").path() == "+" - new QueryStringDecoder("/foo+bar/?").path() == "/foo+bar/" - new QueryStringDecoder("/foo++?index.php").path() == "/foo++" - new QueryStringDecoder("/foo%20+?index.php").path() == "/foo +" - new QueryStringDecoder("/foo+%20").path() == "/foo+ " - } - - void testExcludeFragment() { - expect: - // a 'fragment' after '#' should be cuted (see RFC 3986) - new QueryStringDecoder("?a#anchor").parameters().keySet().iterator().next() == "a" - new QueryStringDecoder("?a=b#anchor").parameters().get("a").get(0) == "b" - new QueryStringDecoder("?#").parameters().isEmpty() - new QueryStringDecoder("?#anchor").parameters().isEmpty() - new QueryStringDecoder("#?a=b#anchor").parameters().isEmpty() - new QueryStringDecoder("?#a=b#anchor").parameters().isEmpty() - } - - void testHashDos() { - when: - StringBuilder buf = new StringBuilder() - buf.append('?') - for (int i = 0; i < 65536; i++) { - buf.append('k') - buf.append(i) - buf.append("=v") - buf.append(i) - buf.append('&') - } - - then: - new QueryStringDecoder(buf.toString()).parameters().size() == 1024 - } - - void testHasPath() { - when: - QueryStringDecoder decoder = new QueryStringDecoder("1=2", false) - Map> params = decoder.parameters() - - then: - decoder.path() == "" - - then: - params.size() == 1 - params.containsKey("1") - List param = params.get("1") - param != null - param.size() == 1 - param.get(0) == "2" - } - - void testUrlDecoding() throws Exception { - when: - final String caffe = new String( - // "Caffé" but instead of putting the literal E-acute in the - // source file, we directly use the UTF-8 encoding so as to - // not rely on the platform's default encoding (not portable). - new byte[] {'C', 'a', 'f', 'f', (byte) 0xC3, (byte) 0xA9}, - "UTF-8") - final String[] tests = [ - // Encoded -> Decoded or error message substring - "", "", - "foo", "foo", - "f+o", "f o", - "f++", "f ", - "fo%", "unterminated escape sequence at index 2 of: fo%", - "%42", "B", - "%5f", "_", - "f%4", "unterminated escape sequence at index 1 of: f%4", - "%x2", "invalid hex byte 'x2' at index 1 of '%x2'", - "%4x", "invalid hex byte '4x' at index 1 of '%4x'", - "Caff%C3%A9", caffe, - "случайный праздник", "случайный праздник", - "случайный%20праздник", "случайный праздник", - "случайный%20праздник%20%E2%98%BA", "случайный праздник ☺", - ] - - then: - for (int i = 0; i < tests.length; i += 2) { - final String encoded = tests[i] - final String expected = tests[i + 1] - try { - final String decoded = QueryStringDecoder.decodeComponent(encoded) - assert decoded == expected - } catch (IllegalArgumentException e) { - assert e.getMessage() == expected - } - } - } - - private static void assertQueryString(String expected, String actual) { - assertQueryString(expected, actual, false) - } - - private static void assertQueryString(String expected, String actual, boolean semicolonIsNormalChar) { - QueryStringDecoder ed = new QueryStringDecoder(expected, StandardCharsets.UTF_8, true, - 1024, semicolonIsNormalChar) - QueryStringDecoder ad = new QueryStringDecoder(actual, StandardCharsets.UTF_8, true, - 1024, semicolonIsNormalChar) - assert ad.path() == ed.path() - assert ad.parameters() == ed.parameters() - } - - // See #189 - void testURI() { - when: - URI uri = URI.create("http://localhost:8080/foo?param1=value1¶m2=value2¶m3=value3") - QueryStringDecoder decoder = new QueryStringDecoder(uri) - - then: - decoder.path() == "/foo" - decoder.rawPath() == "/foo" - decoder.rawQuery() == "param1=value1¶m2=value2¶m3=value3" - Map> params = decoder.parameters() - params.size() == 3 - Iterator>> entries = params.entrySet().iterator() - - when: - Entry> entry = entries.next() - - then: - entry.getKey() == "param1" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value1" - - when: - entry = entries.next() - - then: - entry.getKey() == "param2" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value2" - - when: - entry = entries.next() - - then: - entry.getKey() == "param3" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value3" - - !entries.hasNext() - } - - // See #189 - void testURISlashPath() { - when: - URI uri = URI.create("http://localhost:8080/?param1=value1¶m2=value2¶m3=value3") - QueryStringDecoder decoder = new QueryStringDecoder(uri) - - then: - decoder.path() == "/" - decoder.rawPath() == "/" - decoder.rawQuery() == "param1=value1¶m2=value2¶m3=value3" - - Map> params = decoder.parameters() - params.size() == 3 - Iterator>> entries = params.entrySet().iterator() - - when: - Entry> entry = entries.next() - - then: - entry.getKey() == "param1" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value1" - - when: - entry = entries.next() - - then: - entry.getKey() == "param2" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value2" - - when: - entry = entries.next() - - then: - entry.getKey() == "param3" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value3" - - !entries.hasNext() - } - - // See #189 - void testURINoPath() { - when: - URI uri = URI.create("http://localhost:8080?param1=value1¶m2=value2¶m3=value3") - QueryStringDecoder decoder = new QueryStringDecoder(uri) - - then: - decoder.path() == "" - decoder.rawPath() == "" - decoder.rawQuery() == "param1=value1¶m2=value2¶m3=value3" - - Map> params = decoder.parameters() - params.size() == 3 - Iterator>> entries = params.entrySet().iterator() - - when: - Entry> entry = entries.next() - - then: - entry.getKey() == "param1" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value1" - - when: - entry = entries.next() - - then: - entry.getKey() == "param2" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value2" - - when: - entry = entries.next() - - then: - entry.getKey() == "param3" - entry.getValue().size() == 1 - entry.getValue().get(0) == "value3" - - !entries.hasNext() - } - - // See https://github.com/netty/netty/issues/1833 - void testURI2() { - when: - URI uri = URI.create("http://foo.com/images;num=10?query=name;value=123") - QueryStringDecoder decoder = new QueryStringDecoder(uri) - - then: - decoder.path() == "/images;num=10" - decoder.rawPath() == "/images;num=10" - decoder.rawQuery() == "query=name;value=123" - - Map> params = decoder.parameters() - params.size() == 2 - Iterator>> entries = params.entrySet().iterator() - - when: - Entry> entry = entries.next() - - then: - entry.getKey() == "query" - entry.getValue().size() == 1 - entry.getValue().get(0) == "name" - - when: - entry = entries.next() - - then: - entry.getKey() == "value" - entry.getValue().size() == 1 - entry.getValue().get(0) == "123" - - !entries.hasNext() - } - - void testEmptyStrings() { - when: - QueryStringDecoder pathSlash = new QueryStringDecoder("path/") - - then: - pathSlash.rawPath() == "path/" - pathSlash.rawQuery() == "" - QueryStringDecoder pathQuestion = new QueryStringDecoder("path?") - pathQuestion.rawPath() == "path" - pathQuestion.rawQuery() == "" - QueryStringDecoder empty = new QueryStringDecoder("") - empty.rawPath() == "" - empty.rawQuery() == "" - } - -} diff --git a/http-poja-common/src/test/resources/logback.xml b/http-poja-common/src/test/resources/logback.xml deleted file mode 100644 index ef2b2f918..000000000 --- a/http-poja-common/src/test/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - System.err - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - diff --git a/http-poja-test/build.gradle b/http-poja-test/build.gradle deleted file mode 100644 index 93906ee35..000000000 --- a/http-poja-test/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -plugins { - id("io.micronaut.build.internal.servlet.module") -} - -dependencies { - implementation(projects.micronautHttpPojaCommon) - api(mn.micronaut.inject.java) - api(mn.micronaut.http.client) - - testImplementation(mn.micronaut.jackson.databind) - testImplementation(projects.micronautHttpPojaApache) -} - -micronautBuild { - binaryCompatibility { - enabled.set(false) - } -} diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java deleted file mode 100644 index 90cc876c0..000000000 --- a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.test; - -import io.micronaut.context.ApplicationContext; -import io.micronaut.context.annotation.Replaces; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.env.Environment; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.poja.PojaHttpServerlessApplication; -import io.micronaut.runtime.ApplicationConfiguration; -import io.micronaut.runtime.EmbeddedApplication; -import io.micronaut.runtime.server.EmbeddedServer; -import jakarta.inject.Singleton; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.net.MalformedURLException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URI; -import java.net.URL; -import java.nio.CharBuffer; -import java.nio.channels.AsynchronousCloseException; -import java.nio.channels.Channels; -import java.nio.channels.Pipe; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * An embedded server that uses {@link PojaHttpServerlessApplication} as application. - * It can be used for testing POJA serverless applications the same way a normal micronaut - * server would be tested. - * - *

It delegates to {@link io.micronaut.http.poja.PojaHttpServerlessApplication} by creating 2 - * pipes to communicate with the client and simplifies reading and writing to them.

- * - * @author Andriy Dmytruk - */ -@Singleton -@Requires(env = Environment.TEST) -@Replaces(EmbeddedApplication.class) -public class TestingServerlessEmbeddedApplication implements EmbeddedServer { - - private static final SecureRandom RANDOM = new SecureRandom(); - - private PojaHttpServerlessApplication application; - - private AtomicBoolean isRunning = new AtomicBoolean(false); - private int port; - private ServerSocket serverSocket; - private OutputStream serverInput; - private InputStream serverOutput; - - private Pipe inputPipe; - private Pipe outputPipe; - private Thread serverThread; - - /** - * Default constructor. - * - * @param application The application context - */ - public TestingServerlessEmbeddedApplication( - PojaHttpServerlessApplication application - ) { - this.application = application; - } - - private void createServerSocket() { - IOException exception = null; - for (int i = 0; i < 100; ++i) { - port = RANDOM.nextInt(10000, 20000); - try { - serverSocket = new ServerSocket(port); - return; - } catch (IOException e) { - exception = e; - } - } - throw new RuntimeException("Could not bind to port " + port, exception); - } - - @Override - public TestingServerlessEmbeddedApplication start() { - if (isRunning.compareAndSet(true, true)) { - return this; // Already running - } - createServerSocket(); - - try { - inputPipe = Pipe.open(); - outputPipe = Pipe.open(); - serverInput = Channels.newOutputStream(inputPipe.sink()); - serverOutput = Channels.newInputStream(outputPipe.source()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - // Run the request handling on a new thread - serverThread = new Thread(() -> { - try { - application.start( - Channels.newInputStream(inputPipe.source()), - Channels.newOutputStream(outputPipe.sink()) - ); - } catch (RuntimeException e) { - // The exception happens since socket is closed when context is destroyed - if (!(e.getCause() instanceof AsynchronousCloseException)) { - throw e; - } - } - }); - serverThread.start(); - - // Run the thread that sends requests to the server - new Thread(() -> { - while (!serverSocket.isClosed()) { - try (Socket socket = serverSocket.accept()) { - String request = readInputStream(socket.getInputStream()); - serverInput.write(request.getBytes()); - serverInput.write(new byte[]{'\n'}); - serverInput.flush(); - - String response = readInputStream(serverOutput); - socket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1)); - socket.getOutputStream().flush(); - } catch (java.net.SocketException ignored) { - // Socket closed - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - }).start(); - - return this; - } - - @Override - public @NonNull TestingServerlessEmbeddedApplication stop() { - application.stop(); - try { - serverSocket.close(); - inputPipe.sink().close(); - inputPipe.source().close(); - outputPipe.sink().close(); - outputPipe.source().close(); - serverThread.interrupt(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return this; - } - - @Override - public boolean isRunning() { - return isRunning.get(); - } - - /** - * Get the port. - * - * @return The port - */ - public int getPort() { - return port; - } - - @Override - public String getHost() { - return "localhost"; - } - - @Override - public String getScheme() { - return "http"; - } - - @Override - public URL getURL() { - try { - return getURI().toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - @Override - public URI getURI() { - return URI.create("http://localhost:" + getPort()); - } - - @SuppressWarnings("java:S3776" /* Reduce cognitive complexity warning */) - private String readInputStream(InputStream inputStream) { - // Read with non-UTF charset in case there is binary data and we need to write it back - BufferedReader input = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.ISO_8859_1)); - - StringBuilder result = new StringBuilder(); - - boolean body = false; - int expectedSize = -1; - int currentSize = 0; - CharBuffer buffer = CharBuffer.allocate(1024); - String lastLine = ""; - - while (expectedSize < 0 || currentSize < expectedSize) { - buffer.clear(); - try { - int length = input.read(buffer); - if (length < 0) { - break; - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - buffer.flip(); - - List lines = split(buffer.toString()); - for (int i = 0; i < lines.size(); ++i) { - String line = lines.get(i); - if (i != 0) { - lastLine = line; - } else { - lastLine = lastLine + line; - } - if (body) { - currentSize += line.length(); - } - result.append(line); - if (i < lines.size() - 1) { - result.append("\n"); - if (body) { - currentSize += 1; - } - if (lastLine.toLowerCase(Locale.ENGLISH).startsWith("content-length: ")) { - expectedSize = Integer.parseInt(lastLine.substring("content-length: ".length()).trim()); - } - if (lastLine.trim().isEmpty()) { - body = true; - if (expectedSize < 0) { - expectedSize = 0; - } - } - } - } - } - - return result.toString(); - } - - private List split(String value) { - // Java split can remove empty lines, so we need this - List result = new ArrayList<>(); - int startI = 0; - for (int i = 0; i < value.length(); ++i) { - if (value.charAt(i) == (char) '\n') { - result.add(value.substring(startI, i)); - startI = i + 1; - } - } - result.add(value.substring(startI)); - return result; - } - - @Override - public ApplicationContext getApplicationContext() { - return application.getApplicationContext(); - } - - @Override - public ApplicationConfiguration getApplicationConfiguration() { - return application.getApplicationConfiguration(); - } -} diff --git a/http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy b/http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy deleted file mode 100644 index 3ca113392..000000000 --- a/http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy +++ /dev/null @@ -1,118 +0,0 @@ -package io.micronaut.http.poja.test - - -import io.micronaut.core.annotation.NonNull -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus -import io.micronaut.http.MediaType -import io.micronaut.http.annotation.* -import io.micronaut.http.client.HttpClient -import io.micronaut.http.client.annotation.Client -import io.micronaut.http.client.exceptions.HttpClientResponseException -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("/test").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("/test").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("/test/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 POST method with unused body"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.POST("/test/unused-body", null).header("Host", "h")) - - then: - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == "Success!" - } - - void "test PUT method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.PUT("/test/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" - } - - /** - * A controller for testing. - */ - @Controller(value = "/test", produces = MediaType.TEXT_PLAIN, consumes = MediaType.ALL) - static class TestController { - - @Get - String index() { - return "Hello, Micronaut Without Netty!\n" - } - - @Delete - void delete() { - System.err.println("Delete called") - } - - @Post("/{name}") - @Status(HttpStatus.CREATED) - String create(@NonNull String name) { - return "Hello, " + name + "\n" - } - - @Post("/unused-body") - String createUnusedBody() { - return "Success!" - } - - @Put("/{name}") - @Status(HttpStatus.OK) - String update(@NonNull String name) { - return "Hello, " + name + "!\n" - } - - } - -} diff --git a/settings.gradle b/settings.gradle index 0fe362eff..88da66713 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,12 +31,8 @@ include 'servlet-engine' include 'http-server-jetty' include 'http-server-undertow' include 'http-server-tomcat' -include 'http-poja-common' -include 'http-poja-apache' -include 'http-poja-test' include 'test-suite-http-server-tck-tomcat' include 'test-suite-http-server-tck-undertow' include 'test-suite-http-server-tck-jetty' -include 'test-suite-http-server-tck-poja-apache' include 'test-suite-kotlin-jetty' -include 'test-sample-poja' + diff --git a/test-sample-poja/README.md b/test-sample-poja/README.md deleted file mode 100644 index 9d57e1c32..000000000 --- a/test-sample-poja/README.md +++ /dev/null @@ -1,39 +0,0 @@ -## Plain Old Java Application (POJA) using Micronaut HTTP Framework - -`micronaut-http-poja` module provides an implementation of the Micronaut HTTP Framework for Plain Old Java Applications (POJA). -Such applications can be integrated with Server frameworks such as Unix Super Server (aka Inetd). - -## Sample Application - -This is sample showing an example of using the HTTP POJA module (`micronaut-http-poja`) for serverless applications. - -## Tests - -The tests have `micronaut-http-poja-test` dependency that simplifies the implementation - -## Running - -To run this sample use: -```shell -gradle :micronaut-test-sample-poja:run --console=plain -``` - -Then provide the request in Standard input of the console: -```shell -GET / HTTP/1.1 -Host: h - - -``` - -Get the response: -```shell -HTTP/1.1 200 Ok -Date: Thu, 27 Jun 2024 20:31:09 GMT -Content-Type: text/plain -Content-Length: 32 - -Hello, Micronaut Without Netty! - -``` - diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle deleted file mode 100644 index d1148b6b3..000000000 --- a/test-sample-poja/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright © 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -plugins { - id("io.micronaut.build.internal.servlet.base") - id("application") - id("groovy") -} - -dependencies { - implementation(projects.micronautHttpPojaApache) - implementation(mnLogging.slf4j.simple) - implementation(mn.micronaut.jackson.databind) - annotationProcessor(mn.micronaut.inject.java) - - testImplementation(projects.micronautHttpPojaTest) - testImplementation(mn.micronaut.jackson.databind) - testImplementation(mnTest.micronaut.test.spock) - - testImplementation(mn.micronaut.inject.groovy.test) - testImplementation(mn.micronaut.inject.java) - testImplementation(mn.micronaut.inject.groovy) -} - -run { - mainClass.set("io.micronaut.http.poja.sample.Application") - standardInput = System.in - standardOutput = System.out -} - -test { - useJUnitPlatform() -} diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java deleted file mode 100644 index a37b56865..000000000 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.sample; - -import io.micronaut.runtime.Micronaut; - -/** - * This program demonstrates how to use Micronaut HTTP Router without Netty. - * It reads HTTP requests from stdin and writes HTTP responses to stdout. - * - * @author Sahoo. - */ -public class Application { - - public static void main(String[] args) { - Micronaut.run(Application.class, args); - } - -} - 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 deleted file mode 100644 index 96d511c73..000000000 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.poja.sample; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.MediaType; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Delete; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import io.micronaut.http.annotation.Put; -import io.micronaut.http.annotation.Status; - -/** - * A controller for testing. - * - * @author Sahoo. - */ -@Controller(value = "/", produces = MediaType.TEXT_PLAIN, consumes = MediaType.ALL) -public class TestController { - - @Get - public final String index() { - return "Hello, Micronaut Without Netty!\n"; - } - - @Delete - public final void delete() { - System.err.println("Delete called"); - } - - @Post("/{name}") - @Status(HttpStatus.CREATED) - public final String create(@NonNull String name, HttpRequest request) { - return "Hello, " + name + "\n"; - } - - @Put("/{name}") - @Status(HttpStatus.OK) - public final String update(@NonNull String name) { - return "Hello, " + name + "!\n"; - } - -} - 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 8fc6cbd2a..000000000 --- a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy +++ /dev/null @@ -1,71 +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.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" - } - -} diff --git a/test-suite-http-server-tck-poja-apache/build.gradle b/test-suite-http-server-tck-poja-apache/build.gradle deleted file mode 100644 index d8050432a..000000000 --- a/test-suite-http-server-tck-poja-apache/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("io.micronaut.build.internal.servlet.http-server-tck-module") -} - -dependencies { - testRuntimeOnly(mnValidation.micronaut.validation) - testImplementation(projects.micronautHttpPojaApache) - testImplementation(projects.micronautHttpPojaTest) -} diff --git a/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java deleted file mode 100644 index c9d90c7d2..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.tck.poja; - -import org.junit.platform.suite.api.ExcludeClassNamePatterns; -import org.junit.platform.suite.api.SelectPackages; -import org.junit.platform.suite.api.Suite; -import org.junit.platform.suite.api.SuiteDisplayName; - -@Suite -@SelectPackages({ - "io.micronaut.http.server.tck.tests" -}) -@SuiteDisplayName("HTTP Server TCK for POJA") -@ExcludeClassNamePatterns({ - // 13 tests of 188 fail - // JSON error is not parsed - "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", - "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", - "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", - // See https://github.com/micronaut-projects/micronaut-oracle-cloud/issues/925 - "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", - // Proxying is probably not supported. There is no request concurrency - "io.micronaut.http.server.tck.tests.FilterProxyTest", -}) -public class PojaApacheServerTestSuite { -} diff --git a/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java deleted file mode 100644 index dfc5e339d..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.tck.poja; - -import io.micronaut.context.ApplicationContext; -import io.micronaut.context.env.Environment; -import io.micronaut.core.type.Argument; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.client.BlockingHttpClient; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.HttpClientConfiguration; -import io.micronaut.http.poja.test.TestingServerlessEmbeddedApplication; -import io.micronaut.http.tck.ServerUnderTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Map; -import java.util.Optional; - -@SuppressWarnings("java:S2187") -public class PojaApacheServerUnderTest implements ServerUnderTest { - - private static final Logger LOG = LoggerFactory.getLogger(PojaApacheServerUnderTest.class); - - private final ApplicationContext applicationContext; - private final TestingServerlessEmbeddedApplication application; - private final BlockingHttpClient client; - private final int port; - - public PojaApacheServerUnderTest(Map properties) { - properties.put("micronaut.server.context-path", "/"); - properties.put("endpoints.health.service-ready-indicator-enabled", StringUtils.FALSE); - properties.put("endpoints.refresh.enabled", StringUtils.FALSE); - properties.put("micronaut.security.enabled", StringUtils.FALSE); - applicationContext = ApplicationContext - .builder(Environment.FUNCTION, Environment.TEST) - .eagerInitConfiguration(true) - .eagerInitSingletons(true) - .properties(properties) - .deduceEnvironment(false) - .start(); - application = applicationContext.findBean(TestingServerlessEmbeddedApplication.class) - .orElseThrow(() -> new IllegalStateException("TestingServerlessApplication bean is required")); - application.start(); - port = application.getPort(); - try { - client = HttpClient.create( - new URL("http://localhost:" + port), - applicationContext.getBean(HttpClientConfiguration.class) - ).toBlocking(); - } catch (MalformedURLException e) { - throw new RuntimeException("Could not create HttpClient", e); - } - } - - @Override - public HttpResponse exchange(HttpRequest request, Argument bodyType) { - HttpResponse response = client.exchange(request, bodyType); - if (LOG.isDebugEnabled()) { - LOG.debug("Response status: {}", response.getStatus()); - } - return response; - } - - @Override - public HttpResponse exchange(HttpRequest request, Argument bodyType, Argument errorType) { - return exchange(request, bodyType); - } - - @Override - public ApplicationContext getApplicationContext() { - return applicationContext; - } - - @Override - public Optional getPort() { - return Optional.of(port); - } - - @Override - public void close() throws IOException { - applicationContext.close(); - client.close(); - } -} diff --git a/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java deleted file mode 100644 index 19c28093e..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http.server.tck.poja; - -import io.micronaut.http.tck.ServerUnderTest; -import io.micronaut.http.tck.ServerUnderTestProvider; - -import java.util.Map; - -public class PojaApacheServerUnderTestProvider implements ServerUnderTestProvider { - - @Override - public ServerUnderTest getServer(Map properties) { - return new PojaApacheServerUnderTest(properties); - } - -} diff --git a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json deleted file mode 100644 index e565aba42..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "name": "io.micronaut.http.server.tck.tests.BodyTest$Point", - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "io.micronaut.http.server.tck.tests.ConsumesTest$Pojo", - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "io.micronaut.http.server.tck.tests.MissingBodyAnnotationTest$Dto", - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "io.micronaut.http.hateoas.JsonError", - "allDeclaredConstructors": true - }, - { - "name": "io.micronaut.http.hateoas.Resource", - "allDeclaredConstructors": true - }, - { - "name": "io.micronaut.http.hateoas.GenericResource", - "allDeclaredConstructors": true - } -] diff --git a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json deleted file mode 100644 index ae9383d82..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "resources": { - "includes": [ - { "pattern": "assets/hello.txt" }, - { "pattern": "\\Qlogback.xml\\E" } - ] - } -} diff --git a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider deleted file mode 100644 index ed820b5ae..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.server.tck.poja.PojaApacheServerUnderTestProvider