From 9135e6e25a4c35c270e13da0082e91805071bc07 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 May 2024 14:16:47 +0200 Subject: [PATCH 001/180] Make disabling async a valid option not just for testing (#700) For virtual threads you don't really need async enabled. This adds an option to disable it. --- .../micronaut/servlet/jetty/JettyFactory.java | 5 ++- .../jetty/JettyResponseEncoderSpec.groovy | 2 +- .../servlet/tomcat/TomcatFactory.java | 4 +-- .../servlet/undertow/UndertowFactory.java | 4 +-- .../servlet/http/ServletConfiguration.java | 9 +++++ .../engine/MicronautServletConfiguration.java | 34 ++++++++++++++++++- 6 files changed, 49 insertions(+), 9 deletions(-) diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index c8e04b108..559bf2f06 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -117,10 +117,9 @@ protected Server jettyServer( final ServletHolder servletHolder = new ServletHolder(new DefaultMicronautServlet(applicationContext)); contextHandler.addServlet(servletHolder, configuration.getMapping()); - Boolean isAsync = applicationContext.getEnvironment() - .getProperty("micronaut.server.testing.async", Boolean.class, true); + boolean isAsync = configuration.isAsyncSupported(); if (Boolean.FALSE.equals(isAsync)) { - LOG.warn("Async support disabled for testing purposes."); + LOG.debug("Servlet async mode is disabled"); } servletHolder.setAsyncSupported(isAsync); diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyResponseEncoderSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyResponseEncoderSpec.groovy index 3379a41d7..ecd95a74b 100644 --- a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyResponseEncoderSpec.groovy +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyResponseEncoderSpec.groovy @@ -23,7 +23,7 @@ import spock.lang.Specification @MicronautTest @Property(name = "spec.name", value = SPEC_NAME) -@Property(name = "micronaut.server.testing.async", value = "false") +@Property(name = "micronaut.servlet.async-supported", value = "false") class JettyResponseEncoderSpec extends Specification { private static final String SPEC_NAME = "JettyResponseEncoderSpec" diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 8658fda18..57c1aa99a 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -106,9 +106,9 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration new DefaultMicronautServlet(getApplicationContext()) ); - Boolean isAsync = getApplicationContext().getEnvironment().getProperty("micronaut.server.testing.async", Boolean.class, true); + boolean isAsync = configuration.isAsyncSupported(); if (Boolean.FALSE.equals(isAsync)) { - LOG.warn("Async support disabled for testing purposes."); + LOG.debug("Servlet async mode is disabled"); } servlet.setAsyncSupported(isAsync); servlet.addMapping(configuration.getMapping()); diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index 8df0dceb6..c2fee72a3 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -222,9 +222,9 @@ public void release() { } } ); - Boolean isAsync = getApplicationContext().getEnvironment().getProperty("micronaut.server.testing.async", Boolean.class, true); + boolean isAsync = servletConfiguration.isAsyncSupported(); if (Boolean.FALSE.equals(isAsync)) { - LOG.warn("Async support disabled for testing purposes."); + LOG.debug("Servlet async mode is disabled"); } servletInfo.setAsyncSupported(isAsync); servletInfo.addMapping(servletConfiguration.getMapping()); diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java index 798e3ef4b..cc6ab9128 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java @@ -29,4 +29,13 @@ public interface ServletConfiguration { * @return True if it is. */ boolean isAsyncFileServingEnabled(); + + /** + * Whether to do request processing asynchronously by default (defaults to {@code true}). + * @return True whether async is enabled + * @since 4.8.0 + */ + default boolean isAsyncSupported() { + return true; + } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java index 754704bc5..5e0a16360 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java @@ -17,10 +17,13 @@ import io.micronaut.context.annotation.ConfigurationInject; import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Property; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.Named; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.servlet.http.ServletConfiguration; @@ -46,6 +49,8 @@ public class MicronautServletConfiguration implements Named, ServletConfiguratio private final String name; private boolean asyncFileServingEnabled = true; + private boolean asyncSupported = true; + /** * Default constructor. @@ -73,6 +78,33 @@ public MicronautServletConfiguration( } } + @Override + public boolean isAsyncSupported() { + return asyncSupported; + } + + /** + * Set whether async is supported or not. + * @param asyncSupported True if async is supported. + */ + public void setAsyncSupported(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + /** + * Legacy property to disable async for testing. + * + * @param asyncSupported Is async supported + * @deprecated Use {@link #setAsyncSupported(boolean)} instead + */ + @Deprecated(forRemoval = true, since = "4.8.0") + @Property(name = "micronaut.server.testing.async") + public void setTestAsyncSupported(@Nullable Boolean asyncSupported) { + if (asyncSupported != null) { + this.asyncSupported = asyncSupported; + } + } + /** * @return The servlet mapping. */ @@ -103,6 +135,6 @@ public void setAsyncFileServingEnabled(boolean enabled) { @Override public boolean isAsyncFileServingEnabled() { - return asyncFileServingEnabled; + return asyncSupported && asyncFileServingEnabled; } } From ab6b5f919922555ce8387728af958274aa2552e9 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 May 2024 16:41:10 +0200 Subject: [PATCH 002/180] Support Virtual Threads in Jetty & Tomcat (#701) Allows enabling virtual thread support for Jetty and Tomcat. Undertow has issues with Virtual threads so not implemented. See spring-projects/spring-boot#39812 Co-authored-by: Sergio del Amo --------- Co-authored-by: Sergio del Amo --- .../micronaut/servlet/jetty/JettyFactory.java | 32 +++++- .../jetty/JettyVirtualThreadSpec.groovy | 21 ++++ .../servlet/tomcat/TomcatFactory.java | 108 ++++++++++-------- .../tomcat/TomcatVirtualThreadEnabler.java | 49 ++++++++ .../tomcat/TomcatVirtualThreadSpec.groovy | 20 ++++ .../servlet/http/ServletConfiguration.java | 12 ++ .../engine/MicronautServletConfiguration.java | 15 +++ 7 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy create mode 100644 http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java create mode 100644 http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index 559bf2f06..920b4c84e 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -20,15 +20,20 @@ import io.micronaut.context.annotation.Primary; import io.micronaut.context.env.Environment; import io.micronaut.context.exceptions.ConfigurationException; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.io.ResourceResolver; import io.micronaut.core.io.socket.SocketUtils; import io.micronaut.http.ssl.ClientAuthentication; import io.micronaut.http.ssl.SslConfiguration; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.scheduling.LoomSupport; +import io.micronaut.scheduling.TaskExecutors; import io.micronaut.servlet.engine.DefaultMicronautServlet; import io.micronaut.servlet.engine.MicronautServletConfiguration; import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; import jakarta.inject.Singleton; +import java.util.concurrent.ExecutorService; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -43,6 +48,7 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -111,7 +117,7 @@ protected Server jettyServer( final Integer port = getConfiguredPort(); String contextPath = getContextPath(); - Server server = new Server(); + Server server = newServer(applicationContext, configuration); final ServletContextHandler contextHandler = new ServletContextHandler(server, contextPath, false, false); final ServletHolder servletHolder = new ServletHolder(new DefaultMicronautServlet(applicationContext)); @@ -202,11 +208,31 @@ protected Server jettyServer( return server; } + /** + * Create a new server instance. + * @param applicationContext The application context + * @param configuration The configuration + * @return The server + */ + protected @NonNull Server newServer(@NonNull ApplicationContext applicationContext, @NonNull MicronautServletConfiguration configuration) { + Server server; + if (configuration.isEnableVirtualThreads() && LoomSupport.isSupported()) { + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setVirtualThreadsExecutor( + applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)) + ); + server = new Server(threadPool); + } else { + server = new Server(); + } + return server; + } + /** * For each static resource configuration, create a {@link ContextHandler} that serves the static resources. * - * @param config - * @return + * @param config The static resource configuration + * @return the context handler */ private ContextHandler toHandler(ServletStaticResourceConfiguration config) { Resource[] resourceArray = config.getPaths().stream() diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy new file mode 100644 index 000000000..e95f9bdea --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyVirtualThreadSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.servlet.jetty + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.thread.QueuedThreadPool +import spock.lang.Requires +import spock.lang.Specification + +@MicronautTest +@Requires({ jvm.java21 }) +class JettyVirtualThreadSpec extends Specification { + @Inject Server server + + void "test virtual thread enabled on JDK 21+"() { + expect: + server.threadPool instanceof QueuedThreadPool + server.threadPool.virtualThreadsExecutor != null + + } +} diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 57c1aa99a..1297a3e9d 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -15,6 +15,10 @@ */ package io.micronaut.servlet.tomcat; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; +import jakarta.inject.Named; import java.io.File; import java.util.List; @@ -48,6 +52,7 @@ @Factory public class TomcatFactory extends ServletServerFactory { + private static final String HTTPS = "HTTPS"; private static final Logger LOG = LoggerFactory.getLogger(TomcatFactory.class); /** @@ -77,12 +82,16 @@ public TomcatConfiguration getServerConfiguration() { * The Tomcat server bean. * * @param connector The connector + * @param httpsConnector The HTTPS connector * @param configuration The servlet configuration * @return The Tomcat server */ @Singleton @Primary - protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration configuration) { + protected Tomcat tomcatServer( + Connector connector, + @Named(HTTPS) @Nullable Connector httpsConnector, + MicronautServletConfiguration configuration) { configuration.setAsyncFileServingEnabled(false); Tomcat tomcat = new Tomcat(); tomcat.setHostname(getConfiguredHost()); @@ -118,48 +127,7 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration configuration.getMultipartConfigElement() .ifPresent(servlet::setMultipartConfigElement); - SslConfiguration sslConfiguration = getSslConfiguration(); - if (sslConfiguration.isEnabled()) { - String protocol = sslConfiguration.getProtocol().orElse("TLS"); - int sslPort = sslConfiguration.getPort(); - if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) { - sslPort = 0; - } - Connector httpsConnector = new Connector(); - SSLHostConfig sslHostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED); - sslHostConfig.addCertificate(certificate); - httpsConnector.addSslHostConfig(sslHostConfig); - httpsConnector.setPort(sslPort); - httpsConnector.setSecure(true); - httpsConnector.setScheme("https"); - httpsConnector.setProperty("clientAuth", "false"); - httpsConnector.setProperty("sslProtocol", protocol); - httpsConnector.setProperty("SSLEnabled", "true"); - sslConfiguration.getCiphers().ifPresent(cyphers -> - sslHostConfig.setCiphers(String.join(",", cyphers)) - ); - sslConfiguration.getClientAuthentication().ifPresent(ca -> - httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true") - ); - - - SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore(); - keyStoreConfig.getPassword().ifPresent(certificate::setCertificateKeystorePassword); - keyStoreConfig.getPath().ifPresent(certificate::setCertificateKeystoreFile); - keyStoreConfig.getProvider().ifPresent(certificate::setCertificateKeystorePassword); - keyStoreConfig.getType().ifPresent(certificate::setCertificateKeystoreType); - - SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore(); - trustStore.getPassword().ifPresent(sslHostConfig::setTruststorePassword); - trustStore.getPath().ifPresent(sslHostConfig::setTruststoreFile); - trustStore.getProvider().ifPresent(sslHostConfig::setTruststoreProvider); - trustStore.getType().ifPresent(sslHostConfig::setTruststoreType); - - SslConfiguration.KeyConfiguration keyConfig = sslConfiguration.getKey(); - keyConfig.getAlias().ifPresent(certificate::setCertificateKeyAlias); - keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword); - + if (httpsConnector != null) { tomcat.getService().addConnector(httpsConnector); } @@ -167,7 +135,7 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration } /** - * @return Create the protocol. + * @return Create the connector. */ @Singleton @Primary @@ -177,4 +145,54 @@ protected Connector tomcatConnector() { return tomcatConnector; } -} + /** + * The HTTPS connector. + * @param sslConfiguration The SSL configuration. + * @return The SSL connector + */ + @Singleton + @Named(HTTPS) + @Requires(property = SslConfiguration.PREFIX + ".enabled", value = StringUtils.TRUE) + protected Connector sslConnector(SslConfiguration sslConfiguration) { + String protocol = sslConfiguration.getProtocol().orElse("TLS"); + int sslPort = sslConfiguration.getPort(); + if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) { + sslPort = 0; + } + Connector httpsConnector = new Connector(); + SSLHostConfig sslHostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED); + sslHostConfig.addCertificate(certificate); + httpsConnector.addSslHostConfig(sslHostConfig); + httpsConnector.setPort(sslPort); + httpsConnector.setSecure(true); + httpsConnector.setScheme("https"); + httpsConnector.setProperty("clientAuth", "false"); + httpsConnector.setProperty("sslProtocol", protocol); + httpsConnector.setProperty("SSLEnabled", "true"); + sslConfiguration.getCiphers().ifPresent(cyphers -> + sslHostConfig.setCiphers(String.join(",", cyphers)) + ); + sslConfiguration.getClientAuthentication().ifPresent(ca -> + httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true") + ); + + + SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore(); + keyStoreConfig.getPassword().ifPresent(certificate::setCertificateKeystorePassword); + keyStoreConfig.getPath().ifPresent(certificate::setCertificateKeystoreFile); + keyStoreConfig.getProvider().ifPresent(certificate::setCertificateKeystorePassword); + keyStoreConfig.getType().ifPresent(certificate::setCertificateKeystoreType); + + SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore(); + trustStore.getPassword().ifPresent(sslHostConfig::setTruststorePassword); + trustStore.getPath().ifPresent(sslHostConfig::setTruststoreFile); + trustStore.getProvider().ifPresent(sslHostConfig::setTruststoreProvider); + trustStore.getType().ifPresent(sslHostConfig::setTruststoreType); + + SslConfiguration.KeyConfiguration keyConfig = sslConfiguration.getKey(); + keyConfig.getAlias().ifPresent(certificate::setCertificateKeyAlias); + keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword); + return httpsConnector; + } + } diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java new file mode 100644 index 000000000..28dc0984d --- /dev/null +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatVirtualThreadEnabler.java @@ -0,0 +1,49 @@ +/* + * 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.servlet.tomcat; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.servlet.http.ServletConfiguration; +import jakarta.inject.Singleton; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.ProtocolHandler; +import org.apache.tomcat.util.threads.VirtualThreadExecutor; + +/** + * Enables virtual thread configuration if enabled. + */ +@Requires(sdk = Requires.Sdk.JAVA, version = "21") +@Singleton +class TomcatVirtualThreadEnabler implements BeanCreatedEventListener { + private final ServletConfiguration servletConfiguration; + + public TomcatVirtualThreadEnabler(ServletConfiguration servletConfiguration) { + this.servletConfiguration = servletConfiguration; + } + + @Override + public Connector onCreated(@NonNull BeanCreatedEvent event) { + Connector connector = event.getBean(); + if (servletConfiguration.isEnableVirtualThreads()) { + ProtocolHandler protocolHandler = connector.getProtocolHandler(); + protocolHandler.setExecutor(new VirtualThreadExecutor("tomcat-handler-")); + } + return connector; + } +} diff --git a/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy new file mode 100644 index 000000000..839257399 --- /dev/null +++ b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatVirtualThreadSpec.groovy @@ -0,0 +1,20 @@ +package io.micronaut.servlet.tomcat + + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.apache.catalina.startup.Tomcat +import org.apache.tomcat.util.threads.VirtualThreadExecutor +import spock.lang.Requires +import spock.lang.Specification + +@MicronautTest +@Requires({ jvm.java21 }) +class TomcatVirtualThreadSpec extends Specification { + @Inject Tomcat server + + void "test virtual thread enabled on JDK 21+"() { + expect: + server.connector.protocolHandler.executor instanceof VirtualThreadExecutor + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java index cc6ab9128..bd6bb1a74 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java @@ -38,4 +38,16 @@ public interface ServletConfiguration { default boolean isAsyncSupported() { return true; } + + /** + * Whether to enable virtual thread support if available. + * + *

If virtual threads are not available this option does nothing.

+ * + * @return True if they should be enabled + * @since 4.8.0 + */ + default boolean isEnableVirtualThreads() { + return true; + } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java index 5e0a16360..7369f0d78 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java @@ -50,6 +50,7 @@ public class MicronautServletConfiguration implements Named, ServletConfiguratio private boolean asyncFileServingEnabled = true; private boolean asyncSupported = true; + private boolean enableVirtualThreads = true; /** @@ -105,6 +106,20 @@ public void setTestAsyncSupported(@Nullable Boolean asyncSupported) { } } + @Override + public boolean isEnableVirtualThreads() { + return this.enableVirtualThreads; + } + + /** + * Whether virtual threads are enabled. + * @param enableVirtualThreads True if they are enabled + * @since 4.8.0 + */ + public void setEnableVirtualThreads(boolean enableVirtualThreads) { + this.enableVirtualThreads = enableVirtualThreads; + } + /** * @return The servlet mapping. */ From baa9b7191fee729b7b81fa774ccb4e11c1dfd6af Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 May 2024 09:25:21 +0200 Subject: [PATCH 003/180] Make servlet more flexible / support servlet annotations (#702) * Allows the DefaultMicronautServlet to be replaced / customized * Adds support for Servlet annotation model (@WebServlet, @WebFilter and @WebListener) * Unifies the logic across implementations to use MicronautServletInitialier * Correctly implement dual-protocol setting * Fix random port binding --- http-server-jetty/build.gradle | 1 + .../micronaut/servlet/jetty/JettyFactory.java | 288 +++++++++++------- .../micronaut/servlet/jetty/JettyServer.java | 3 +- .../jetty/JettyServletAnnotationSpec.groovy | 31 ++ .../servlet/jetty/MyExtraFilter.java | 28 ++ .../servlet/jetty/MyExtraServlet.java | 25 ++ .../servlet/jetty/MyFilterFactory.java | 29 ++ .../micronaut/servlet/jetty/MyLastFilter.java | 25 ++ .../micronaut/servlet/jetty/MyListener.java | 15 + .../servlet/tomcat/TomcatFactory.java | 133 +++++--- .../servlet/undertow/UndertowFactory.java | 121 ++++---- servlet-engine/build.gradle | 1 + .../engine/DefaultMicronautServlet.java | 7 + .../engine/MicronautServletConfiguration.java | 1 - .../engine/annotation/ServletBean.java | 85 ++++++ .../engine/annotation/ServletFilterBean.java | 87 ++++++ .../MicronautServletInitializer.java | 212 ++++++++++++- servlet-processor/build.gradle.kts | 13 + .../processor/ServletAnnotationVisitor.java | 81 +++++ .../annotation/processor/WebFilterMapper.java | 48 +++ .../processor/WebListenerMapper.java | 38 +++ .../processor/WebServletMapper.java | 48 +++ ...cronaut.inject.annotation.AnnotationMapper | 3 + ...icronaut.inject.visitor.TypeElementVisitor | 1 + .../src/main/resources/logback.xml | 19 ++ .../processor/ServletAnnotationSpec.groovy | 73 +++++ settings.gradle | 1 + src/main/docs/guide/servletAnnotation.adoc | 18 ++ src/main/docs/guide/toc.yml | 1 + 29 files changed, 1221 insertions(+), 215 deletions(-) create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy create mode 100644 http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraFilter.java create mode 100644 http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraServlet.java create mode 100644 http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java create mode 100644 http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyLastFilter.java create mode 100644 http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyListener.java create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java create mode 100644 servlet-processor/build.gradle.kts create mode 100644 servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java create mode 100644 servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebFilterMapper.java create mode 100644 servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebListenerMapper.java create mode 100644 servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebServletMapper.java create mode 100644 servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper create mode 100644 servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor create mode 100644 servlet-processor/src/main/resources/logback.xml create mode 100644 servlet-processor/src/test/groovy/io/micronaut/servlet/annotation/processor/ServletAnnotationSpec.groovy create mode 100644 src/main/docs/guide/servletAnnotation.adoc diff --git a/http-server-jetty/build.gradle b/http-server-jetty/build.gradle index b6cb158df..20b9b5739 100644 --- a/http-server-jetty/build.gradle +++ b/http-server-jetty/build.gradle @@ -7,6 +7,7 @@ dependencies { testImplementation libs.bcpkix testCompileOnly(mnValidation.micronaut.validation.processor) testAnnotationProcessor(mnValidation.micronaut.validation.processor) + testAnnotationProcessor(projects.micronautServletProcessor) testImplementation(mnValidation.micronaut.validation) testImplementation(mnSerde.micronaut.serde.jackson) testImplementation(mnLogging.logback.classic) diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index 920b4c84e..b8454890a 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -15,24 +15,30 @@ */ package io.micronaut.servlet.jetty; +import static io.micronaut.core.util.StringUtils.isEmpty; + import io.micronaut.context.ApplicationContext; import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Primary; import io.micronaut.context.env.Environment; import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.ResourceResolver; -import io.micronaut.core.io.socket.SocketUtils; +import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.ssl.ClientAuthentication; import io.micronaut.http.ssl.SslConfiguration; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.scheduling.LoomSupport; import io.micronaut.scheduling.TaskExecutors; -import io.micronaut.servlet.engine.DefaultMicronautServlet; import io.micronaut.servlet.engine.MicronautServletConfiguration; +import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; import jakarta.inject.Singleton; +import java.io.IOException; +import java.util.List; +import java.util.stream.Stream; import java.util.concurrent.ExecutorService; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.HttpConfiguration; @@ -44,19 +50,10 @@ import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; -import java.util.stream.Stream; - -import static io.micronaut.core.util.StringUtils.isEmpty; /** * Factory for the Jetty server. @@ -69,8 +66,6 @@ public class JettyFactory extends ServletServerFactory { public static final String RESOURCE_BASE = "resourceBase"; - private static final Logger LOG = LoggerFactory.getLogger(JettyFactory.class); - private final JettyConfiguration jettyConfiguration; /** @@ -83,17 +78,17 @@ public class JettyFactory extends ServletServerFactory { * @param staticResourceConfigurations The static resource configs */ public JettyFactory( - ResourceResolver resourceResolver, - JettyConfiguration serverConfiguration, - SslConfiguration sslConfiguration, - ApplicationContext applicationContext, - List staticResourceConfigurations) { + ResourceResolver resourceResolver, + JettyConfiguration serverConfiguration, + SslConfiguration sslConfiguration, + ApplicationContext applicationContext, + List staticResourceConfigurations) { super( - resourceResolver, - serverConfiguration, - sslConfiguration, - applicationContext, - staticResourceConfigurations + resourceResolver, + serverConfiguration, + sslConfiguration, + applicationContext, + staticResourceConfigurations ); this.jettyConfiguration = serverConfiguration; } @@ -106,12 +101,35 @@ public JettyFactory( * @param jettySslConfiguration The Jetty SSL config * @return The Jetty server bean */ + protected Server jettyServer( + ApplicationContext applicationContext, + MicronautServletConfiguration configuration, + JettyConfiguration.JettySslConfiguration jettySslConfiguration + ) { + return jettyServer( + applicationContext, + configuration, + jettySslConfiguration, + applicationContext.getBean(MicronautServletInitializer.class) + ); + } + + /** + * Builds the Jetty server bean. + * + * @param applicationContext This application context + * @param configuration The servlet configuration + * @param jettySslConfiguration The Jetty SSL config + * @param micronautServletInitializer The micronaut servlet initializer + * @return The Jetty server bean + */ @Singleton @Primary protected Server jettyServer( - ApplicationContext applicationContext, - MicronautServletConfiguration configuration, - JettyConfiguration.JettySslConfiguration jettySslConfiguration + ApplicationContext applicationContext, + MicronautServletConfiguration configuration, + JettyConfiguration.JettySslConfiguration jettySslConfiguration, + MicronautServletInitializer micronautServletInitializer ) { final String host = getConfiguredHost(); final Integer port = getConfiguredPort(); @@ -119,99 +137,147 @@ protected Server jettyServer( Server server = newServer(applicationContext, configuration); - final ServletContextHandler contextHandler = new ServletContextHandler(server, contextPath, false, false); - final ServletHolder servletHolder = new ServletHolder(new DefaultMicronautServlet(applicationContext)); - contextHandler.addServlet(servletHolder, configuration.getMapping()); + final ServletContextHandler contextHandler = newJettyContext(server, contextPath); + configureServletInitializer(server, contextHandler, micronautServletInitializer); + + final SslConfiguration sslConfiguration = getSslConfiguration(); + ServerConnector https = null; + if (sslConfiguration.isEnabled()) { + https = newHttpsConnector(server, sslConfiguration, jettySslConfiguration); + + } + final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(jettyConfiguration.getHttpConfiguration())); + http.setPort(port); + http.setHost(host); + configureConnectors(server, http, https); + + return server; + } + + /** + * Create the HTTPS connector. + * + * @param server The server + * @param sslConfiguration The SSL configuration + * @param jettySslConfiguration The Jetty SSL configuration + * @return The server connector + */ + protected @NonNull ServerConnector newHttpsConnector( + @NonNull Server server, + @NonNull SslConfiguration sslConfiguration, + @NonNull JettyConfiguration.JettySslConfiguration jettySslConfiguration) { + ServerConnector https; + final HttpConfiguration httpConfig = jettyConfiguration.getHttpConfiguration(); + int securePort = sslConfiguration.getPort(); + if (securePort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) { + securePort = 0; // random port + } + httpConfig.setSecurePort(securePort); + + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - boolean isAsync = configuration.isAsyncSupported(); - if (Boolean.FALSE.equals(isAsync)) { - LOG.debug("Servlet async mode is disabled"); + ClientAuthentication clientAuth = sslConfiguration.getClientAuthentication().orElse(ClientAuthentication.NEED); + switch (clientAuth) { + case WANT: + sslContextFactory.setWantClientAuth(true); + break; + case NEED: + default: + sslContextFactory.setNeedClientAuth(true); } - servletHolder.setAsyncSupported(isAsync); - configuration.getMultipartConfigElement().ifPresent(multipartConfiguration -> - servletHolder.getRegistration().setMultipartConfig(multipartConfiguration) + sslConfiguration.getProtocol().ifPresent(sslContextFactory::setProtocol); + sslConfiguration.getProtocols().ifPresent(sslContextFactory::setIncludeProtocols); + sslConfiguration.getCiphers().ifPresent(sslConfiguration::setCiphers); + final SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore(); + keyStoreConfig.getPassword().ifPresent(sslContextFactory::setKeyStorePassword); + keyStoreConfig.getPath().ifPresent(path -> { + if (path.startsWith(ServletStaticResourceConfiguration.CLASSPATH_PREFIX)) { + String cp = path.substring(ServletStaticResourceConfiguration.CLASSPATH_PREFIX.length()); + sslContextFactory.setKeyStorePath(Resource.newClassPathResource(cp).getURI().toString()); + } else { + sslContextFactory.setKeyStorePath(path); + } + }); + keyStoreConfig.getProvider().ifPresent(sslContextFactory::setKeyStoreProvider); + keyStoreConfig.getType().ifPresent(sslContextFactory::setKeyStoreType); + SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore(); + trustStore.getPassword().ifPresent(sslContextFactory::setTrustStorePassword); + trustStore.getType().ifPresent(sslContextFactory::setTrustStoreType); + trustStore.getPath().ifPresent(path -> { + if (path.startsWith(ServletStaticResourceConfiguration.CLASSPATH_PREFIX)) { + String cp = path.substring(ServletStaticResourceConfiguration.CLASSPATH_PREFIX.length()); + sslContextFactory.setTrustStorePath(Resource.newClassPathResource(cp).getURI().toString()); + } else { + sslContextFactory.setTrustStorePath(path); + } + }); + trustStore.getProvider().ifPresent(sslContextFactory::setTrustStoreProvider); + + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(jettySslConfiguration); + https = new ServerConnector(server, + new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), + new HttpConnectionFactory(httpsConfig) ); + https.setPort(securePort); + return https; + } + + /** + * Configures the servlet initializer + * + * @param server The server + * @param contextHandler The context handler + * @param micronautServletInitializer The initializer + */ + protected void configureServletInitializer(Server server, ServletContextHandler contextHandler, MicronautServletInitializer micronautServletInitializer) { + contextHandler.addServletContainerInitializer(micronautServletInitializer); List resourceHandlers = Stream.concat( - getStaticResourceConfigurations().stream().map(this::toHandler), - Stream.of(contextHandler) + getStaticResourceConfigurations().stream().map(this::toHandler), + Stream.of(contextHandler) ).toList(); HandlerList handlerList = new HandlerList(resourceHandlers.toArray(new ContextHandler[0])); server.setHandler(handlerList); + } - final SslConfiguration sslConfiguration = getSslConfiguration(); - if (sslConfiguration.isEnabled()) { - final HttpConfiguration httpConfig = jettyConfiguration.getHttpConfiguration(); - int securePort = sslConfiguration.getPort(); - if (securePort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) { - securePort = SocketUtils.findAvailableTcpPort(); - } - httpConfig.setSecurePort(securePort); - - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + /** + * Create the Jetty context. + * + * @param server The server + * @param contextPath The context path + * @return The handler + */ + protected @NonNull ServletContextHandler newJettyContext(@NonNull Server server, @NonNull String contextPath) { + return new ServletContextHandler(server, contextPath, false, false); + } - ClientAuthentication clientAuth = sslConfiguration.getClientAuthentication().orElse(ClientAuthentication.NEED); - switch (clientAuth) { - case WANT: - sslContextFactory.setWantClientAuth(true); - break; - case NEED: - default: - sslContextFactory.setNeedClientAuth(true); + /** + * Configures the server connectors. + * + * @param server The server + * @param http The HTTP connector + * @param https The HTTPS connector if configured. + */ + protected void configureConnectors(@NonNull Server server, @NonNull ServerConnector http, @Nullable ServerConnector https) { + HttpServerConfiguration serverConfiguration = getServerConfiguration(); + if (https != null) { + server.addConnector(https); // must be first + if (serverConfiguration.isDualProtocol()) { + server.addConnector(http); } - - sslConfiguration.getProtocol().ifPresent(sslContextFactory::setProtocol); - sslConfiguration.getProtocols().ifPresent(sslContextFactory::setIncludeProtocols); - sslConfiguration.getCiphers().ifPresent(sslConfiguration::setCiphers); - final SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore(); - keyStoreConfig.getPassword().ifPresent(sslContextFactory::setKeyStorePassword); - keyStoreConfig.getPath().ifPresent(path -> { - if (path.startsWith(ServletStaticResourceConfiguration.CLASSPATH_PREFIX)) { - String cp = path.substring(ServletStaticResourceConfiguration.CLASSPATH_PREFIX.length()); - sslContextFactory.setKeyStorePath(Resource.newClassPathResource(cp).getURI().toString()); - } else { - sslContextFactory.setKeyStorePath(path); - } - }); - keyStoreConfig.getProvider().ifPresent(sslContextFactory::setKeyStoreProvider); - keyStoreConfig.getType().ifPresent(sslContextFactory::setKeyStoreType); - SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore(); - trustStore.getPassword().ifPresent(sslContextFactory::setTrustStorePassword); - trustStore.getType().ifPresent(sslContextFactory::setTrustStoreType); - trustStore.getPath().ifPresent(path -> { - if (path.startsWith(ServletStaticResourceConfiguration.CLASSPATH_PREFIX)) { - String cp = path.substring(ServletStaticResourceConfiguration.CLASSPATH_PREFIX.length()); - sslContextFactory.setTrustStorePath(Resource.newClassPathResource(cp).getURI().toString()); - } else { - sslContextFactory.setTrustStorePath(path); - } - }); - trustStore.getProvider().ifPresent(sslContextFactory::setTrustStoreProvider); - - HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); - httpsConfig.addCustomizer(jettySslConfiguration); - ServerConnector https = new ServerConnector(server, - new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), - new HttpConnectionFactory(httpsConfig) - ); - https.setPort(securePort); - server.addConnector(https); - + } else { + server.addConnector(http); } - final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(jettyConfiguration.getHttpConfiguration())); - http.setPort(port); - http.setHost(host); - server.addConnector(http); - - return server; } /** * Create a new server instance. + * * @param applicationContext The application context - * @param configuration The configuration + * @param configuration The configuration * @return The server */ protected @NonNull Server newServer(@NonNull ApplicationContext applicationContext, @NonNull MicronautServletConfiguration configuration) { @@ -236,18 +302,18 @@ protected Server jettyServer( */ private ContextHandler toHandler(ServletStaticResourceConfiguration config) { Resource[] resourceArray = config.getPaths().stream() - .map(path -> { - if (path.startsWith(ServletStaticResourceConfiguration.CLASSPATH_PREFIX)) { - String cp = path.substring(ServletStaticResourceConfiguration.CLASSPATH_PREFIX.length()); - return Resource.newClassPathResource(cp); - } else { - try { - return Resource.newResource(path); - } catch (IOException e) { - throw new ConfigurationException("Static resource path doesn't exist: " + path, e); - } + .map(path -> { + if (path.startsWith(ServletStaticResourceConfiguration.CLASSPATH_PREFIX)) { + String cp = path.substring(ServletStaticResourceConfiguration.CLASSPATH_PREFIX.length()); + return Resource.newClassPathResource(cp); + } else { + try { + return Resource.newResource(path); + } catch (IOException e) { + throw new ConfigurationException("Static resource path doesn't exist: " + path, e); } - }).toArray(Resource[]::new); + } + }).toArray(Resource[]::new); String path = config.getMapping(); if (path.endsWith("/**")) { diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java index 63c178338..c35660683 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java @@ -61,7 +61,8 @@ protected void stopServer() throws Exception { @Override public int getPort() { - return getServer().getURI().getPort(); + Server server = getServer(); + return server.getURI().getPort(); } @Override diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy new file mode 100644 index 000000000..fef911e11 --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy @@ -0,0 +1,31 @@ +package io.micronaut.servlet.jetty + +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +class JettyServletAnnotationSpec extends Specification { + @Inject + @Client("/") + HttpClient rxClient + + @Inject MyListener myListener + + void "test extra servlet"() { + expect: + rxClient.toBlocking().retrieve("/extra-servlet", String) == 'My Servlet!' + } + + void "test extra filter"() { + expect: + rxClient.toBlocking().retrieve("/extra-filter", String) == 'My Filter!' + } + + void "test extra listener"() { + expect: + myListener.initialized + } +} diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraFilter.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraFilter.java new file mode 100644 index 000000000..e374c4202 --- /dev/null +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraFilter.java @@ -0,0 +1,28 @@ +package io.micronaut.servlet.jetty; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.GenericServlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebServlet; +import java.io.IOException; +import java.io.PrintWriter; + +@WebFilter(value = "/extra-filter/*") +public class MyExtraFilter extends GenericFilter { + + @Override + public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain) throws IOException, ServletException { + if (!Boolean.TRUE.equals(request.getAttribute("runFirst"))) { + throw new IllegalStateException("Should have run second"); + } + request.setAttribute("runSecond", true); + PrintWriter writer = res.getWriter(); + res.setContentType("text/plain"); + writer.write("My Filter!"); + writer.flush(); + } +} diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraServlet.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraServlet.java new file mode 100644 index 000000000..3ba5ee12a --- /dev/null +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyExtraServlet.java @@ -0,0 +1,25 @@ +package io.micronaut.servlet.jetty; + +import jakarta.servlet.GenericServlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebServlet; +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(value = "/extra-servlet/*") +public class MyExtraServlet extends GenericServlet { + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + if (!Boolean.TRUE.equals(req.getAttribute("runFirst")) || + !Boolean.TRUE.equals(req.getAttribute("runSecond"))) { + throw new IllegalStateException("Should have run last"); + } + try (PrintWriter writer = res.getWriter()) { + res.setContentType("text/plain"); + writer.write("My Servlet!"); + writer.flush(); + }; + } +} diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java new file mode 100644 index 000000000..f779c0c2a --- /dev/null +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java @@ -0,0 +1,29 @@ +package io.micronaut.servlet.jetty; + +import io.micronaut.context.annotation.Factory; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.order.Ordered; +import io.micronaut.servlet.engine.annotation.ServletFilterBean; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; + +@Factory +public class MyFilterFactory { + + @ServletFilterBean(filterName = "another", value = {"/extra-filter/*", "/extra-servlet/*"}) + @Order(Ordered.HIGHEST_PRECEDENCE) + Filter myOtherFilter() { + return new GenericFilter() { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { + request.setAttribute("runFirst", true); + chain.doFilter(request, response); + } + }; + } +} diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyLastFilter.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyLastFilter.java new file mode 100644 index 000000000..748b15742 --- /dev/null +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyLastFilter.java @@ -0,0 +1,25 @@ +package io.micronaut.servlet.jetty; + + +import io.micronaut.core.annotation.Order; +import io.micronaut.core.order.Ordered; +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import java.io.IOException; + +@WebFilter(value = {"/extra-filter/*", "/extra-servlet/*"}) +@Order(Ordered.LOWEST_PRECEDENCE) +public class MyLastFilter extends GenericFilter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (!Boolean.TRUE.equals(request.getAttribute("runFirst"))) { + throw new IllegalStateException("Should have run last"); + } + request.setAttribute("runSecond", true); + chain.doFilter(request, response); + } +} diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyListener.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyListener.java new file mode 100644 index 000000000..4f7a7d002 --- /dev/null +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyListener.java @@ -0,0 +1,15 @@ +package io.micronaut.servlet.jetty; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.annotation.WebListener; + +@WebListener +public class MyListener implements ServletContextListener { + public static boolean initialized; + + @Override + public void contextInitialized(ServletContextEvent sce) { + initialized = true; + } +} diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 1297a3e9d..ef9997307 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -16,9 +16,12 @@ package io.micronaut.servlet.tomcat; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.qualifiers.Qualifiers; import jakarta.inject.Named; +import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; import java.io.File; import java.util.List; @@ -34,14 +37,12 @@ import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; import jakarta.inject.Singleton; +import java.util.Set; import org.apache.catalina.Context; -import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Factory for the {@link Tomcat} instance. @@ -53,7 +54,6 @@ public class TomcatFactory extends ServletServerFactory { private static final String HTTPS = "HTTPS"; - private static final Logger LOG = LoggerFactory.getLogger(TomcatFactory.class); /** * Default constructor. @@ -65,11 +65,11 @@ public class TomcatFactory extends ServletServerFactory { * @param staticResourceConfigurations The static resource configs */ protected TomcatFactory( - ResourceResolver resourceResolver, - TomcatConfiguration serverConfiguration, - SslConfiguration sslConfiguration, - ApplicationContext applicationContext, - List staticResourceConfigurations) { + ResourceResolver resourceResolver, + TomcatConfiguration serverConfiguration, + SslConfiguration sslConfiguration, + ApplicationContext applicationContext, + List staticResourceConfigurations) { super(resourceResolver, serverConfiguration, sslConfiguration, applicationContext, staticResourceConfigurations); } @@ -81,56 +81,108 @@ public TomcatConfiguration getServerConfiguration() { /** * The Tomcat server bean. * - * @param connector The connector - * @param httpsConnector The HTTPS connector + * @param connector The connector * @param configuration The servlet configuration * @return The Tomcat server */ + protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration configuration) { + return tomcatServer( + connector, + getApplicationContext().getBean(Connector.class, Qualifiers.byName(HTTPS)), + configuration, + getApplicationContext().getBean(MicronautServletInitializer.class)); + } + + /** + * The Tomcat server bean. + * + * @param connector The connector + * @param httpsConnector The HTTPS connectors + * @param configuration The servlet configuration + * @param servletInitializer The servlet initializer + * @return The Tomcat server + */ @Singleton @Primary protected Tomcat tomcatServer( Connector connector, @Named(HTTPS) @Nullable Connector httpsConnector, - MicronautServletConfiguration configuration) { + MicronautServletConfiguration configuration, + MicronautServletInitializer servletInitializer) { configuration.setAsyncFileServingEnabled(false); - Tomcat tomcat = new Tomcat(); - tomcat.setHostname(getConfiguredHost()); - final String contextPath = getContextPath(); - tomcat.getHost().setAutoDeploy(false); - tomcat.setConnector(connector); - final String cp = contextPath != null && !contextPath.equals("/") ? contextPath : ""; - final Context context = tomcat.addContext(cp, "/"); + Tomcat tomcat = newTomcat(); + final Context context = newTomcatContext(tomcat); - // add required folder - File docBaseFile = new File(context.getDocBase()); - if (!docBaseFile.isAbsolute()) { - docBaseFile = new File(((org.apache.catalina.Host) context.getParent()).getAppBaseFile(), docBaseFile.getPath()); - } - docBaseFile.mkdirs(); + configureServletInitializer(context, servletInitializer); + configureConnectors(tomcat, connector, httpsConnector); - final Wrapper servlet = Tomcat.addServlet( - context, - configuration.getName(), - new DefaultMicronautServlet(getApplicationContext()) - ); + return tomcat; + } - boolean isAsync = configuration.isAsyncSupported(); - if (Boolean.FALSE.equals(isAsync)) { - LOG.debug("Servlet async mode is disabled"); - } - servlet.setAsyncSupported(isAsync); - servlet.addMapping(configuration.getMapping()); + /** + * Configure the Micronaut servlet intializer. + * + * @param context The context + * @param servletInitializer The intializer + */ + protected void configureServletInitializer(Context context, MicronautServletInitializer servletInitializer) { getStaticResourceConfigurations().forEach(config -> - servlet.addMapping(config.getMapping()) + servletInitializer.addMicronautServletMapping(config.getMapping()) + ); + context.addServletContainerInitializer( + servletInitializer, Set.of(DefaultMicronautServlet.class) ); - configuration.getMultipartConfigElement() - .ifPresent(servlet::setMultipartConfigElement); + } + + /** + * Configures the available connectors. + * + * @param tomcat The tomcat instance + * @param httpConnector The HTTP connector + * @param httpsConnector The HTTPS connector + */ + protected void configureConnectors(@NonNull Tomcat tomcat, @NonNull Connector httpConnector, @Nullable Connector httpsConnector) { + TomcatConfiguration serverConfiguration = getServerConfiguration(); if (httpsConnector != null) { tomcat.getService().addConnector(httpsConnector); + if (serverConfiguration.isDualProtocol()) { + tomcat.getService().addConnector(httpConnector); + } + } else { + tomcat.setConnector(httpConnector); + } + } + + /** + * Create a new context. + * + * @param tomcat The tomcat instance + * @return The context + */ + protected @NonNull Context newTomcatContext(@NonNull Tomcat tomcat) { + final String contextPath = getContextPath(); + final String cp = contextPath != null && !contextPath.equals("/") ? contextPath : ""; + final Context context = tomcat.addContext(cp, "/"); + // add required folder + File docBaseFile = new File(context.getDocBase()); + if (!docBaseFile.isAbsolute()) { + docBaseFile = new File(((org.apache.catalina.Host) context.getParent()).getAppBaseFile(), docBaseFile.getPath()); } + docBaseFile.mkdirs(); + return context; + } + /** + * Create a new tomcat server. + * + * @return The tomcat server + */ + protected @NonNull Tomcat newTomcat() { + Tomcat tomcat = new Tomcat(); + tomcat.getHost().setAutoDeploy(false); + tomcat.setHostname(getConfiguredHost()); return tomcat; } @@ -147,6 +199,7 @@ protected Connector tomcatConnector() { /** * The HTTPS connector. + * * @param sslConfiguration The SSL configuration. * @return The SSL connector */ @@ -195,4 +248,4 @@ protected Connector sslConnector(SslConfiguration sslConfiguration) { keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword); return httpsConnector; } - } +} diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index c2fee72a3..c901dad1f 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -20,12 +20,11 @@ import io.micronaut.context.annotation.Primary; import io.micronaut.context.env.Environment; import io.micronaut.core.io.ResourceResolver; -import io.micronaut.core.io.socket.SocketUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.http.server.exceptions.ServerStartupException; import io.micronaut.http.ssl.SslConfiguration; -import io.micronaut.servlet.engine.DefaultMicronautServlet; import io.micronaut.servlet.engine.MicronautServletConfiguration; +import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; import io.undertow.Handlers; @@ -33,17 +32,19 @@ import io.undertow.UndertowOptions; import io.undertow.server.handlers.PathHandler; import io.undertow.servlet.Servlets; -import io.undertow.servlet.api.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xnio.Option; -import org.xnio.Options; - +import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.DeploymentManager; +import io.undertow.servlet.api.InstanceHandle; +import io.undertow.servlet.api.ServletContainerInitializerInfo; import jakarta.inject.Singleton; -import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; import jakarta.servlet.ServletException; import java.util.List; import java.util.Map; +import java.util.Set; +import javax.net.ssl.SSLContext; +import org.xnio.Option; +import org.xnio.Options; /** * Factory for the undertow server. @@ -54,8 +55,6 @@ @Factory public class UndertowFactory extends ServletServerFactory { - private static final Logger LOG = LoggerFactory.getLogger(UndertowFactory.class); - private final UndertowConfiguration configuration; /** @@ -90,10 +89,7 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron final Undertow.Builder builder = configuration.getUndertowBuilder(); int port = getConfiguredPort(); String host = getConfiguredHost(); - builder.addHttpListener( - port, - host - ); + final String cp = getContextPath(); final DeploymentManager deploymentManager = Servlets.defaultContainer().addDeployment(deploymentInfo); @@ -112,16 +108,34 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron if (sslConfiguration.isEnabled()) { int sslPort = sslConfiguration.getPort(); if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) { - sslPort = SocketUtils.findAvailableTcpPort(); + sslPort = 0; // random port } int finalSslPort = sslPort; - build(sslConfiguration).ifPresent(sslContext -> - builder.addHttpsListener( - finalSslPort, - host, - sslContext - )); + SSLContext sslContext = build(sslConfiguration).orElse(null); + if (sslContext != null) { + builder.addHttpsListener( + finalSslPort, + host, + sslContext + ); + if (getServerConfiguration().isDualProtocol()) { + builder.addHttpListener( + port, + host + ); + } + } else { + builder.addHttpListener( + port, + host + ); + } + } else { + builder.addHttpListener( + port, + host + ); } Map serverOptions = configuration.getServerOptions(); @@ -197,47 +211,46 @@ protected Undertow undertowServer(Undertow.Builder builder) { * * @param servletConfiguration The servlet configuration. * @return The deployment info + * @deprecated Use {@link #deploymentInfo(MicronautServletConfiguration, MicronautServletInitializer)} + */ + @Deprecated(forRemoval = true, since = "4.8.0") + protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration) { + return deploymentInfo(servletConfiguration, getApplicationContext().getBean(MicronautServletInitializer.class)); + } + + /** + * The deployment info bean. + * + * @param servletConfiguration The servlet configuration. + * @param servletInitializer The servlet initializer + * @return The deployment info */ @Singleton @Primary - protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration) { + protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration, MicronautServletInitializer servletInitializer) { final String cp = getContextPath(); - - ServletInfo servletInfo = Servlets.servlet( - servletConfiguration.getName(), DefaultMicronautServlet.class, () -> new InstanceHandle() { - - private DefaultMicronautServlet instance; - - @Override - public Servlet getInstance() { - instance = new DefaultMicronautServlet(getApplicationContext()); - return instance; - } - - @Override - public void release() { - if (instance != null) { - instance.destroy(); - } - } - } - ); - boolean isAsync = servletConfiguration.isAsyncSupported(); - if (Boolean.FALSE.equals(isAsync)) { - LOG.debug("Servlet async mode is disabled"); - } - servletInfo.setAsyncSupported(isAsync); - servletInfo.addMapping(servletConfiguration.getMapping()); getStaticResourceConfigurations().forEach(config -> { - servletInfo.addMapping(config.getMapping()); + servletInitializer.addMicronautServletMapping(config.getMapping()); }); - final DeploymentInfo deploymentInfo = Servlets.deployment() + return Servlets.deployment() .setDeploymentName(servletConfiguration.getName()) .setClassLoader(getEnvironment().getClassLoader()) .setContextPath(cp) - .addServlet(servletInfo); - servletConfiguration.getMultipartConfigElement().ifPresent(deploymentInfo::setDefaultMultipartConfig); - return deploymentInfo; + .addServletContainerInitializer(new ServletContainerInitializerInfo( + MicronautServletInitializer.class, + () -> new InstanceHandle<>() { + @Override + public ServletContainerInitializer getInstance() { + return servletInitializer; + } + + @Override + public void release() { + + } + }, + Set.of(MicronautServletInitializer.class) + )); } } diff --git a/servlet-engine/build.gradle b/servlet-engine/build.gradle index 8489ac499..9c9cebc99 100644 --- a/servlet-engine/build.gradle +++ b/servlet-engine/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { annotationProcessor mn.micronaut.graal + annotationProcessor(projects.micronautServletProcessor) api(projects.micronautServletCore) api libs.managed.servlet.api diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java index b93f437d2..5f3f99e3e 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java @@ -20,7 +20,9 @@ import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.TypeHint; +import jakarta.inject.Inject; import jakarta.servlet.ServletContext; +import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -33,6 +35,10 @@ * @since 1.0 */ @TypeHint(DefaultMicronautServlet.class) +@WebServlet( + name = DefaultMicronautServlet.NAME, + loadOnStartup = 1 +) public class DefaultMicronautServlet extends HttpServlet { /** * The name of the servlet. @@ -51,6 +57,7 @@ public class DefaultMicronautServlet extends HttpServlet { * Constructor that takes an application context. * @param applicationContext The application context. */ + @Inject public DefaultMicronautServlet(ApplicationContext applicationContext) { this.applicationContext = Objects.requireNonNull(applicationContext, "The application context cannot be null"); } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java index 7369f0d78..407ef99ec 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java @@ -23,7 +23,6 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.Named; -import io.micronaut.core.util.StringUtils; import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.servlet.http.ServletConfiguration; diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java new file mode 100644 index 000000000..f1e973960 --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java @@ -0,0 +1,85 @@ +/* + * 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.servlet.engine.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Bean; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebServlet; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Variant of {@link jakarta.servlet.annotation.WebServlet} applicable to factory methods. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Bean +public @interface ServletBean { + /** + * The name of the servlet. + * + * @return the name of the servlet + */ + @AliasFor(annotation = WebServlet.class, member = "name") + String name(); + + /** + * The URL patterns of the servlet. + * + * @return the URL patterns of the servlet + */ + @AliasFor(annotation = WebServlet.class, member = "value") + String[] value() default {}; + + /** + * The URL patterns of the servlet. + * + * @return the URL patterns of the servlet + */ + @AliasFor(annotation = WebServlet.class, member = "value") + String[] urlPatterns() default {}; + + /** + * The load-on-startup order of the servlet. + * + * @return the load-on-startup order of the servlet + */ + @AliasFor(annotation = WebServlet.class, member = "loadOnStartup") + int loadOnStartup() default -1; + + /** + * The init parameters of the servlet. + * + * @return the init parameters of the servlet + */ + @AliasFor(annotation = WebServlet.class, member = "initParams") + WebInitParam[] initParams() default {}; + + /** + * Declares whether the servlet supports asynchronous operation mode. + * + * @return {@code true} if the servlet supports asynchronous operation mode + * @see jakarta.servlet.ServletRequest#startAsync + * @see jakarta.servlet.ServletRequest#startAsync( jakarta.servlet.ServletRequest,jakarta.servlet.ServletResponse) + */ + @AliasFor(annotation = WebServlet.class, member = "asyncSupported") + boolean asyncSupported() default false; +} diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java new file mode 100644 index 000000000..9c191347f --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java @@ -0,0 +1,87 @@ +/* + * 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.servlet.engine.annotation; + +import io.micronaut.context.annotation.AliasFor; +import io.micronaut.context.annotation.Bean; +import io.micronaut.core.annotation.AnnotationMetadata; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.annotation.WebFilter; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Variant of {@link jakarta.servlet.annotation.WebFilter} applicable to factory methods. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Bean +public @interface ServletFilterBean { + /** + * The name of the filter. + * + * @return the name of the filter + */ + @AliasFor(annotation = WebFilter.class, member = "filterName") + String filterName(); + + /** + * The names of the servlets to which the filter applies. + * + * @return the names of the servlets to which the filter applies + */ + @AliasFor(annotation = WebFilter.class, member = "servletNames") + String[] servletNames() default {}; + + /** + * The URL patterns to which the filter applies The default value is an empty array. + * + * @return the URL patterns to which the filter applies + */ + @AliasFor(annotation = WebFilter.class, member = AnnotationMetadata.VALUE_MEMBER) + String[] value() default {}; + + /** + * The URL patterns to which the filter applies. + * + * @return the URL patterns to which the filter applies + */ + @AliasFor(annotation = WebFilter.class, member = AnnotationMetadata.VALUE_MEMBER) + String[] urlPatterns() default {}; + + /** + * The dispatcher types to which the filter applies. + * + * @return the dispatcher types to which the filter applies + */ + @AliasFor(annotation = WebFilter.class, member = "dispatcherTypes") + DispatcherType[] dispatcherTypes() default { DispatcherType.REQUEST }; + + /** + * Declares whether the filter supports asynchronous operation mode. + * + * @return {@code true} if the filter supports asynchronous operation mode + * @see jakarta.servlet.ServletRequest#startAsync + * @see jakarta.servlet.ServletRequest#startAsync( jakarta.servlet.ServletRequest,jakarta.servlet.ServletResponse) + */ + @AliasFor(annotation = WebFilter.class, member = "asyncSupported") + boolean asyncSupported() default false; + +} diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/initializer/MicronautServletInitializer.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/initializer/MicronautServletInitializer.java index ffd0e2cbe..e79c9dd0b 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/initializer/MicronautServletInitializer.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/initializer/MicronautServletInitializer.java @@ -17,12 +17,39 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.ApplicationContextBuilder; +import io.micronaut.context.BeanRegistration; +import io.micronaut.context.annotation.Prototype; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.BeanIdentifier; +import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.servlet.engine.DefaultMicronautServlet; import io.micronaut.servlet.engine.MicronautServletConfiguration; +import jakarta.inject.Inject; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.Servlet; import jakarta.servlet.ServletContainerInitializer; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletRegistration; import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebListener; +import jakarta.servlet.annotation.WebServlet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.EventListener; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,25 +64,184 @@ * @author graemerocher * @since 1.0.0 */ +@Prototype public class MicronautServletInitializer implements ServletContainerInitializer { private static final Logger LOG = LoggerFactory.getLogger(MicronautServletInitializer.class); + private static final AnnotationValue EMPTY_WEB_SERVLET = new AnnotationValue<>(WebServlet.class.getName()); + private static final String MEMBER_URL_PATTERNS = "urlPatterns"; + private static final String MEMBER_LOAD_ON_STARTUP = "loadOnStartup"; + private static final String MEMBER_ASYNC_SUPPORTED = "asyncSupported"; + private static final String MEMBER_INIT_PARAMS = "initParams"; + private static final DispatcherType[] DEFAULT_DISPATCHER_TYPES = {DispatcherType.REQUEST}; + private ApplicationContext applicationContext; + private List micronautServletMappings = new ArrayList<>(); + + @Inject + public MicronautServletInitializer(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public MicronautServletInitializer() { + } @Override public void onStartup(Set> c, ServletContext ctx) { - final ApplicationContext applicationContext = buildApplicationContext(ctx) + final ApplicationContext applicationContext = this.applicationContext != null ? this.applicationContext : buildApplicationContext(ctx) .build() .start(); final MicronautServletConfiguration configuration = applicationContext.getBean(MicronautServletConfiguration.class); - final ServletRegistration.Dynamic registration = - ctx.addServlet(configuration.getName(), new DefaultMicronautServlet(applicationContext)); + Collection> servlets = applicationContext.getBeanRegistrations(Servlet.class); + Collection> filters = applicationContext.getBeanRegistrations(Filter.class); + Collection servletListeners = applicationContext.getBeansOfType(EventListener.class, Qualifiers.byStereotype(WebListener.class)); + int servletOrder = 0; + for (BeanRegistration servlet : servlets) { + Servlet servletBean = servlet.getBean(); + String servletName = resolveName(servlet.getIdentifier(), servlet.getBeanDefinition()); + ServletRegistration.Dynamic registration = ctx.addServlet(servletName, servletBean); + servletOrder = configureServletBean( + servlet, + servletName, + configuration, + servletOrder, + registration, + applicationContext + ); + } + for (BeanRegistration beanRegistration : filters) { + handleFilterRegistration(ctx, beanRegistration); + } + + for (EventListener servletListener : servletListeners) { + ctx.addListener(servletListener); + } + + } + + private static void handleFilterRegistration(ServletContext ctx, BeanRegistration beanRegistration) { + Filter filter = beanRegistration.getBean(); + BeanIdentifier identifier = beanRegistration.getIdentifier(); + BeanDefinition beanDefinition = beanRegistration.getBeanDefinition(); + String filterName = resolveName(identifier, beanDefinition); + FilterRegistration.Dynamic registration = ctx.addFilter(filterName, filter); + AnnotationValue webFilterAnn = beanDefinition.findAnnotation(WebFilter.class).orElse(new AnnotationValue<>(WebFilter.class.getName())); + DispatcherType[] dispatcherTypes = webFilterAnn.enumValues("dispatcherTypes", DispatcherType.class); + if (ArrayUtils.isEmpty(dispatcherTypes)) { + dispatcherTypes = DEFAULT_DISPATCHER_TYPES; + } + + @NonNull String[] urlPatterns = ArrayUtils.concat( + webFilterAnn.stringValues(), + webFilterAnn.stringValues(MEMBER_URL_PATTERNS) + ); + @NonNull String[] servletNames = webFilterAnn.stringValues("servletNames"); + EnumSet enumSet; + if (dispatcherTypes.length > 1) { + enumSet = EnumSet.of(dispatcherTypes[0], Arrays.copyOfRange(dispatcherTypes, 1, dispatcherTypes.length)); + } else { + enumSet = EnumSet.of(dispatcherTypes[0]); + } + + if (ArrayUtils.isNotEmpty(urlPatterns)) { + registration.addMappingForUrlPatterns( + enumSet, + true, + urlPatterns + ); + } + if (ArrayUtils.isNotEmpty(servletNames)) { + registration.addMappingForUrlPatterns( + enumSet, + true, + servletNames + ); + } + setInitParams(webFilterAnn, registration); + registration.setAsyncSupported(webFilterAnn.booleanValue(MEMBER_ASYNC_SUPPORTED).orElse(false)); + } + + private static String resolveName(BeanIdentifier identifier, BeanDefinition definition) { + String name = identifier.getName(); + return name.equals("Primary") ? definition.getBeanType().getName() : name; + } - configuration.getMultipartConfigElement().ifPresent(registration::setMultipartConfig); - applicationContext.findBean(ServletSecurityElement.class) + private int configureServletBean(BeanRegistration servlet, String servletName, MicronautServletConfiguration configuration, int order, ServletRegistration.Dynamic registration, ApplicationContext applicationContext) { + BeanDefinition beanDefinition = servlet.getBeanDefinition(); + AnnotationValue webServletAnnotationValue = beanDefinition + .findAnnotation(WebServlet.class) + .orElse(EMPTY_WEB_SERVLET); + boolean isMicronautServlet = DefaultMicronautServlet.NAME.equals(servletName); + @NonNull String[] urlPatterns = getUrlPatterns(webServletAnnotationValue, beanDefinition, isMicronautServlet, configuration); + int loadOnStartup = webServletAnnotationValue.intValue(MEMBER_LOAD_ON_STARTUP).orElse(order++); + boolean isAsyncSupported = webServletAnnotationValue.booleanValue(MEMBER_ASYNC_SUPPORTED).orElse(configuration.isAsyncSupported()); + + registration.addMapping(urlPatterns); + registration.setLoadOnStartup(loadOnStartup); + registration.setAsyncSupported(isAsyncSupported); + setInitParams(webServletAnnotationValue, registration); + MultipartConfigElement multipartConfigElement = getMultipartConfig(beanDefinition, isMicronautServlet, configuration); + if (multipartConfigElement != null) { + registration.setMultipartConfig(multipartConfigElement); + } + ServletSecurity servletSecurity = beanDefinition.synthesizeDeclared(ServletSecurity.class); + if (servletSecurity != null) { + registration.setServletSecurity(new ServletSecurityElement(servletSecurity)); + } else if (isMicronautServlet) { + applicationContext.findBean(ServletSecurityElement.class) .ifPresent(registration::setServletSecurity); - registration.setLoadOnStartup(1); - registration.setAsyncSupported(true); - registration.addMapping(configuration.getMapping()); + } + return order; + } + + private @NonNull String[] getUrlPatterns(AnnotationValue webServletAnnotationValue, BeanDefinition beanDefinition, boolean isMicronautServlet, MicronautServletConfiguration configuration) { + @NonNull String[] urlPatterns = + ArrayUtils.concat( + webServletAnnotationValue.stringValues(), + beanDefinition.stringValues(MEMBER_URL_PATTERNS) + ); + if (ArrayUtils.isEmpty(urlPatterns) && isMicronautServlet) { + urlPatterns = ArrayUtils.concat(micronautServletMappings.toArray(String[]::new), configuration.getMapping()); + } + return urlPatterns; + } + + private static void setInitParams(AnnotationValue webServletAnnotationValue, ServletRegistration.Dynamic registration) { + webServletAnnotationValue.getAnnotations(MEMBER_INIT_PARAMS, WebInitParam.class).forEach(av -> { + av.stringValue("name").ifPresent(name -> + av.stringValue().ifPresent(value -> + registration.setInitParameter(name, value) + ) + ); + }); + } + + private static void setInitParams(AnnotationValue webFilter, FilterRegistration.Dynamic registration) { + webFilter.getAnnotations(MEMBER_INIT_PARAMS, WebInitParam.class).forEach(av -> { + av.stringValue("name").ifPresent(name -> + av.stringValue().ifPresent(value -> + registration.setInitParameter(name, value) + ) + ); + }); + } + + private MultipartConfigElement getMultipartConfig(BeanDefinition beanDefinition, boolean isMicronautServlet, MicronautServletConfiguration configuration) { + return beanDefinition.findAnnotation(MultipartConfig.class) + .map(this::toMultipartElement) + .orElse(isMicronautServlet ? configuration.getMultipartConfigElement().orElse(null) : null); + } + + private MultipartConfigElement toMultipartElement(AnnotationValue annotation) { + String location = annotation.stringValue("location").orElse(""); + long maxFileSize = annotation.longValue("maxFileSize").orElse(-1); + long maxRequestSize = annotation.longValue("maxRequestSize").orElse(-1); + int fileSizeThreshold = annotation.intValue("fileSizeThreshold").orElse(-1); + return new MultipartConfigElement( + location, + maxFileSize, + maxRequestSize, + fileSizeThreshold + ); } /** @@ -81,4 +267,14 @@ protected ApplicationContextBuilder buildApplicationContext(ServletContext ctx) return contextBuilder; } + + /** + * Adds an additional mapping for the micronaut servlet. + * @param mapping The mapping. + */ + public void addMicronautServletMapping(String mapping) { + if (StringUtils.isNotEmpty(mapping)) { + this.micronautServletMappings.add(mapping); + } + } } diff --git a/servlet-processor/build.gradle.kts b/servlet-processor/build.gradle.kts new file mode 100644 index 000000000..cbbe3ded8 --- /dev/null +++ b/servlet-processor/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("io.micronaut.build.internal.servlet.module") +} + +dependencies { + api(libs.managed.servlet.api) + implementation(mn.micronaut.core.processor) + testImplementation(mn.micronaut.inject.java.test) +} + +micronautBuild { + binaryCompatibility.enabled.set(false) +} diff --git a/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java new file mode 100644 index 000000000..6c004c7cc --- /dev/null +++ b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java @@ -0,0 +1,81 @@ +/* + * 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.servlet.annotation.processor; + +import static io.micronaut.core.util.ArrayUtils.concat; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import jakarta.servlet.Filter; +import jakarta.servlet.Servlet; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebListener; +import jakarta.servlet.annotation.WebServlet; +import java.util.Set; + +public class ServletAnnotationVisitor implements TypeElementVisitor { + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public Set getSupportedAnnotationNames() { + return Set.of("jakarta.servlet.*"); + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + if (element.hasDeclaredAnnotation(WebFilter.class) && !element.isAssignable(Filter.class)) { + throw new ProcessingException(element, "Types annotated with @WebFilter must implement jakarta.servlet.Filter"); + } + if (element.hasDeclaredAnnotation(WebServlet.class) && !element.isAssignable(Servlet.class)) { + throw new ProcessingException(element, "Types annotated with @WebServlet must implement jakarta.servlet.Servlet"); + } + if (element.hasDeclaredAnnotation(WebListener.class) && !element.isAssignable(java.util.EventListener.class)) { + throw new ProcessingException(element, "Types annotated with @WebListener must implement java.util.EventListener"); + } + + @NonNull String[] patterns = concat(concat( + element.stringValues(WebFilter.class), + element.stringValues(WebServlet.class, "urlPatterns") + ), + concat( + element.stringValues(WebServlet.class), + element.stringValues(WebServlet.class, "urlPatterns") + )); + for (String pattern : patterns) { + if (pattern.endsWith("/**") && pattern.length() > 3) { + throw new ProcessingException(element, "Servlet Spec 12.2 violation: glob '*' can only exist at end of prefix based matches: bad spec \"" + pattern + "\""); + } + } + } + + @Override + public void visitMethod(MethodElement element, VisitorContext context) { + if (element.hasDeclaredAnnotation(WebFilter.class) && !element.getGenericReturnType().isAssignable(Filter.class)) { + throw new ProcessingException(element, "Methods annotated with @ServletFilterBean must implement jakarta.servlet.Filter"); + } + if (element.hasDeclaredAnnotation(WebServlet.class) && !element.getGenericReturnType().isAssignable(Servlet.class)) { + throw new ProcessingException(element, "Methods annotated with @ServletBean must implement jakarta.servlet.Servlet"); + } + } +} diff --git a/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebFilterMapper.java b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebFilterMapper.java new file mode 100644 index 000000000..af1416f8b --- /dev/null +++ b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebFilterMapper.java @@ -0,0 +1,48 @@ +/* + * 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.servlet.annotation.processor; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.servlet.annotation.WebFilter; +import java.util.List; + +/** + * Allows registering web filters as beans. + */ +public class WebFilterMapper implements TypedAnnotationMapper { + @Override + public Class annotationType() { + return WebFilter.class; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + String name = annotation.stringValue("filterName").orElse(null); + if (StringUtils.isNotEmpty(name)) { + return List.of( + AnnotationValue.builder(Named.class).value(name).build(), + AnnotationValue.builder(Singleton.class).build() + ); + } else { + return List.of(AnnotationValue.builder(Singleton.class).build()); + } + } +} diff --git a/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebListenerMapper.java b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebListenerMapper.java new file mode 100644 index 000000000..f5d4f0289 --- /dev/null +++ b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebListenerMapper.java @@ -0,0 +1,38 @@ +/* + * 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.servlet.annotation.processor; + +import io.micronaut.context.annotation.Bean; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; +import jakarta.servlet.annotation.WebListener; +import java.util.List; + +/** + * Allows registering web listeners as beans. + */ +public class WebListenerMapper implements TypedAnnotationMapper { + @Override + public Class annotationType() { + return WebListener.class; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + return List.of(AnnotationValue.builder(Bean.class).build()); + } +} diff --git a/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebServletMapper.java b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebServletMapper.java new file mode 100644 index 000000000..2a2765986 --- /dev/null +++ b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/WebServletMapper.java @@ -0,0 +1,48 @@ +/* + * 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.servlet.annotation.processor; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.annotation.TypedAnnotationMapper; +import io.micronaut.inject.visitor.VisitorContext; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.servlet.annotation.WebServlet; +import java.util.List; + +/** + * Allows registering web servlets as beans. + */ +public class WebServletMapper implements TypedAnnotationMapper { + @Override + public Class annotationType() { + return WebServlet.class; + } + + @Override + public List> map(AnnotationValue annotation, VisitorContext visitorContext) { + String name = annotation.stringValue("name").orElse(null); + if (StringUtils.isNotEmpty(name)) { + return List.of( + AnnotationValue.builder(Named.class).value(name).build(), + AnnotationValue.builder(Singleton.class).build() + ); + } else { + return List.of(AnnotationValue.builder(Singleton.class).build()); + } + } +} diff --git a/servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper new file mode 100644 index 000000000..a74409185 --- /dev/null +++ b/servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -0,0 +1,3 @@ +io.micronaut.servlet.annotation.processor.WebFilterMapper +io.micronaut.servlet.annotation.processor.WebListenerMapper +io.micronaut.servlet.annotation.processor.WebServletMapper diff --git a/servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 000000000..5e598cccc --- /dev/null +++ b/servlet-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1 @@ +io.micronaut.servlet.annotation.processor.ServletAnnotationVisitor diff --git a/servlet-processor/src/main/resources/logback.xml b/servlet-processor/src/main/resources/logback.xml new file mode 100644 index 000000000..ea89172ea --- /dev/null +++ b/servlet-processor/src/main/resources/logback.xml @@ -0,0 +1,19 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/servlet-processor/src/test/groovy/io/micronaut/servlet/annotation/processor/ServletAnnotationSpec.groovy b/servlet-processor/src/test/groovy/io/micronaut/servlet/annotation/processor/ServletAnnotationSpec.groovy new file mode 100644 index 000000000..b98d5677a --- /dev/null +++ b/servlet-processor/src/test/groovy/io/micronaut/servlet/annotation/processor/ServletAnnotationSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.servlet.annotation.processor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import jakarta.servlet.Servlet + +class ServletAnnotationSpec + extends AbstractTypeElementSpec { + + void "test servlet annotated is bean"() { + given: + def context = buildContext(''' +package test; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.GenericServlet; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebListener; +import jakarta.servlet.annotation.WebServlet; +import java.io.*; + +@WebServlet +class MyServlet extends GenericServlet { + @Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + + } +} + +@WebServlet(name = "test") +class MyServlet2 extends GenericServlet { +@Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + + } +} + +@WebFilter +class Filter1 extends GenericFilter { + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + } +} +@WebFilter(filterName = "two") +class Filter2 extends GenericFilter { + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + } +} + +@WebListener +class Listener implements ServletContextListener { +} +''') + expect: + getBean(context, 'test.MyServlet') + getBean(context, 'test.MyServlet2') + getBean(context, 'test.MyServlet', Qualifiers.byName("MyServlet")) + getBean(context, 'test.MyServlet2', Qualifiers.byName("test")) + getBean(context, 'test.Listener') + getBean(context, 'test.Filter1') + getBean(context, 'test.Filter2') + } +} diff --git a/settings.gradle b/settings.gradle index d3b83a472..518a81d4a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,6 +24,7 @@ micronautBuild { } include 'servlet-bom' +include 'servlet-processor' include 'servlet-core' include 'servlet-engine' include 'http-server-jetty' diff --git a/src/main/docs/guide/servletAnnotation.adoc b/src/main/docs/guide/servletAnnotation.adoc new file mode 100644 index 000000000..e68b8fa85 --- /dev/null +++ b/src/main/docs/guide/servletAnnotation.adoc @@ -0,0 +1,18 @@ +To use the https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/package-summary[Servlet APIs annotations] to register servlets, filters and listeners you first need to add the following annotation processor dependency: + +dependency:io.micronaut.servlet:micronaut-servlet-processor[scope="annotationProcessor"] + +The following annotations can then be used from the Servlet API to add additional servlets, filters and listeners as beans: + +* https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webfilter[@WebFilter] - Applicable to types that implement the `jakarta.servlet.Filter` interface. +* https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webservlet[@WebServlet] - Applicable to types that implement the `jakarta.servlet.Servlet` interface. +* https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/weblistener[@WebListener] - See the annotation javadoc for applicable types. + +The https://docs.micronaut.io/latest/api/io/micronaut/core/annotation/Order.html[@Order] annotation can be used to control registration order and hence filter order. For example a value of https://docs.micronaut.io/latest/api/io/micronaut/core/order/Ordered.html#HIGHEST_PRECEDENCE[io.micronaut.core.order.Ordered.HIGHEST_PRECEDENCE] will run the filter first. + +NOTE: Using `HIGHEST_PRECEDENCE` will prevent any other filter running before your filter. The default position is `0` and `HIGHEST_PRECEDENCE` == `Integer.MIN_VALUE` hence you should consider using constants to the represent the position of your filter that exist somewhere between `HIGHEST_PRECEDENCE` and `LOWEST_PRECEDENCE`. + +In addition, you can use the following annotations on methods of https://docs.micronaut.io/latest/guide/#factories[@Factory beans] to instantiate servlets and filters and register them: + +* ann:servlet.engine.annotation.ServletBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webservlet[@WebServlet] but can be applied to a method of a factory to Register a new servlet. +* ann:servlet.engine.annotation.ServletFilterBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webfilter[@WebFilter] but can be applied to a method of a factory to Register a new filter. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index c9016dd42..4c8558475 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -1,6 +1,7 @@ introduction: Introduction releaseHistory: Release History servletApi: Working with the Servlet API +servletAnnotation: Servlet Annotation Support warDeployment: title: WAR Deployment versioning: Container version considerations From faa4a790ed4696aab7a0fde7f4dce68e7ed2e520 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:36:44 +0200 Subject: [PATCH 004/180] fix(deps): update dependency io.micronaut.session:micronaut-session-bom to v4.3.0 (#691) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33e15ef53..d218cee95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ managed-jetty = '11.0.20' micronaut-reactor = "3.3.0" micronaut-security = "4.6.9" micronaut-serde = "2.8.2" -micronaut-session = "4.2.0" +micronaut-session = "4.3.0" micronaut-validation = "4.4.4" google-cloud-functions = '1.1.0' kotlin = "1.9.22" From 3c8eeede1cb4682008d94479224fbaf430c87719 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:37:19 +0200 Subject: [PATCH 005/180] fix(deps): update dependency io.micronaut.validation:micronaut-validation-bom to v4.5.0 (#692) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d218cee95..1dd30eb17 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ micronaut-reactor = "3.3.0" micronaut-security = "4.6.9" micronaut-serde = "2.8.2" micronaut-session = "4.3.0" -micronaut-validation = "4.4.4" +micronaut-validation = "4.5.0" google-cloud-functions = '1.1.0' kotlin = "1.9.22" micronaut-logging = "1.2.3" From 1ff8583cf66b138a9213f9bc196c6bd86ccc0ad9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:37:33 +0200 Subject: [PATCH 006/180] fix(deps): update dependency io.micronaut.security:micronaut-security-bom to v4.7.0 (#689) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1dd30eb17..1a0d6e37c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ bcpkix = "1.70" managed-jetty = '11.0.20' micronaut-reactor = "3.3.0" -micronaut-security = "4.6.9" +micronaut-security = "4.7.0" micronaut-serde = "2.8.2" micronaut-session = "4.3.0" micronaut-validation = "4.5.0" From 2d9410bdd8bd3563ec1540aefd8a52f2c4886b29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:37:51 +0200 Subject: [PATCH 007/180] fix(deps): update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.9.24 (#696) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a0d6e37c..172ee3bfe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ micronaut-serde = "2.8.2" micronaut-session = "4.3.0" micronaut-validation = "4.5.0" google-cloud-functions = '1.1.0' -kotlin = "1.9.22" +kotlin = "1.9.24" micronaut-logging = "1.2.3" # Micronaut From 0be64d6db69fb1f29cd449da73fbdeabfca595db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:38:56 +0200 Subject: [PATCH 008/180] fix(deps): update dependency io.micronaut.serde:micronaut-serde-bom to v2.9.0 (#690) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Sergio del Amo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 172ee3bfe..0563c9f95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ managed-jetty = '11.0.20' micronaut-reactor = "3.3.0" micronaut-security = "4.7.0" -micronaut-serde = "2.8.2" +micronaut-serde = "2.9.0" micronaut-session = "4.3.0" micronaut-validation = "4.5.0" google-cloud-functions = '1.1.0' From 4bc372367e3f653e621d9074772adffac5d03251 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:39:14 +0200 Subject: [PATCH 009/180] fix(deps): update dependency io.micronaut.gradle:micronaut-gradle-plugin to v4.4.0 (#688) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0563c9f95..a9321f349 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ kotlin = "1.9.24" micronaut-logging = "1.2.3" # Micronaut -micronaut-gradle-plugin = "4.3.5" +micronaut-gradle-plugin = "4.4.0" [libraries] # Core From d4f0678bbda3fd30a6e8213be76c0445ab3621c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 10:14:50 +0200 Subject: [PATCH 010/180] fix(deps): update dependency org.apache.tomcat.embed:tomcat-embed-core to v10.1.24 (#687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9321f349..28a2ac54a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' kotest-runner = '5.8.1' undertow = '2.3.12.Final' -tomcat = '10.1.20' +tomcat = '10.1.24' graal-svm = "23.1.2" bcpkix = "1.70" From 177491478a7969eccef600f574af457c8b8a59c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 16:41:22 +0200 Subject: [PATCH 011/180] fix(deps): update managed.jetty to v11.0.21 (#699) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28a2ac54a..6dd405e98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ tomcat = '10.1.24' graal-svm = "23.1.2" bcpkix = "1.70" -managed-jetty = '11.0.20' +managed-jetty = '11.0.21' micronaut-reactor = "3.3.0" micronaut-security = "4.7.0" From 95a70e4214cf09f6520802a32d332e21c7a65442 Mon Sep 17 00:00:00 2001 From: Charlie Wolf Date: Wed, 22 May 2024 07:41:56 -0700 Subject: [PATCH 012/180] Add web-fragment.xml so that the caller can disable the ServletContextInitializer (if a custom one is desired, for example). (#639) See also https://github.com/spring-projects/spring-framework/commit/d309bb4bbbf41cac2c59808e0addebcaee5b9e29 --- .../src/main/resources/META-INF/web-fragment.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 servlet-engine/src/main/resources/META-INF/web-fragment.xml diff --git a/servlet-engine/src/main/resources/META-INF/web-fragment.xml b/servlet-engine/src/main/resources/META-INF/web-fragment.xml new file mode 100644 index 000000000..caced2535 --- /dev/null +++ b/servlet-engine/src/main/resources/META-INF/web-fragment.xml @@ -0,0 +1,9 @@ + + + + micronaut_servlet + + From 21f42e1f25cd45cc9d693ba58025f3b36e810399 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 16:42:12 +0200 Subject: [PATCH 013/180] fix(deps): update dependency io.undertow:undertow-servlet to v2.3.13.final (#685) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6dd405e98..7be0fe36c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' kotest-runner = '5.8.1' -undertow = '2.3.12.Final' +undertow = '2.3.13.Final' tomcat = '10.1.24' graal-svm = "23.1.2" bcpkix = "1.70" From 30d11c5d14ea16317032fea09c3cd753cf10a79b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 16:42:36 +0200 Subject: [PATCH 014/180] chore(deps): update plugin io.micronaut.build.shared.settings to v7 (#694) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 518a81d4a..f237909f9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '6.7.0' + id 'io.micronaut.build.shared.settings' version '7.0.1' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 2d8a37b76b002d4d0fe6732ed9bf0e0b642e702e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:35:28 +0200 Subject: [PATCH 015/180] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.4.10 (#683) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7be0fe36c..00775dd1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.4.0" +micronaut = "4.4.10" micronaut-docs = "2.0.0" micronaut-test = "4.0.1" From 6cc1267034cd8487e1c9d6f097fdf4b86c636d77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:35:44 +0200 Subject: [PATCH 016/180] chore(deps): update gradle/gradle-build-action action to v3.3.2 (#681) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index cffd16db9..78142de20 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -52,7 +52,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.1.0 + uses: gradle/gradle-build-action@v3.3.2 - name: "❓ Optional setup step" run: | From 2d3afddd0adc4de780c2b7ef6abb1f4eb0956c10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 09:36:00 +0200 Subject: [PATCH 017/180] fix(deps): update dependency io.micronaut.logging:micronaut-logging-bom to v1.3.0 (#682) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00775dd1f..c8ea6c600 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ micronaut-session = "4.3.0" micronaut-validation = "4.5.0" google-cloud-functions = '1.1.0' kotlin = "1.9.24" -micronaut-logging = "1.2.3" +micronaut-logging = "1.3.0" # Micronaut micronaut-gradle-plugin = "4.4.0" From 76188c628d23a4e48c0cfd6b069883aa1147fc69 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 May 2024 11:17:00 +0200 Subject: [PATCH 018/180] Support for http/2 over plaintext for Jetty & Tomcat (#706) --- gradle/libs.versions.toml | 3 + http-server-jetty/build.gradle | 3 + .../micronaut/servlet/jetty/JettyFactory.java | 58 ++++- .../jetty/JettyHttp2OverPlaintextSpec.groovy | 77 ++++++ .../servlet/jetty/JettyHttp2Spec.groovy | 77 ++++++ .../servlet/tomcat/TomcatFactory.java | 7 +- .../TonmcatHttp2OverPlaintextSpec.groovy | 73 ++++++ .../servlet/http/ServletHttpHandler.java | 234 +++++++++--------- .../engine/DefaultServletHttpRequest.java | 13 +- 9 files changed, 425 insertions(+), 120 deletions(-) create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2OverPlaintextSpec.groovy create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy create mode 100644 http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TonmcatHttp2OverPlaintextSpec.groovy diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8ea6c600..8ed536203 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,9 @@ kotlin-reflect = { module = 'org.jetbrains.kotlin:kotlin-reflect' } tomcat-embed-core = { module = 'org.apache.tomcat.embed:tomcat-embed-core', version.ref = 'tomcat' } undertow-servlet = { module = 'io.undertow:undertow-servlet', version.ref = 'undertow' } jetty-servlet = { module = 'org.eclipse.jetty:jetty-servlet', version.ref = 'managed-jetty' } +jetty-http2-server = { module = 'org.eclipse.jetty.http2:http2-server', version.ref = 'managed-jetty' } +jetty-alpn-server = { module = 'org.eclipse.jetty:jetty-alpn-server', version.ref = 'managed-jetty' } +jetty-alpn-conscrypt-server = { module = 'org.eclipse.jetty:jetty-alpn-conscrypt-server', version.ref = 'managed-jetty' } kotest-runner = { module = 'io.kotest:kotest-runner-junit5', version.ref = 'kotest-runner' } bcpkix = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix" } diff --git a/http-server-jetty/build.gradle b/http-server-jetty/build.gradle index 20b9b5739..edf3d7352 100644 --- a/http-server-jetty/build.gradle +++ b/http-server-jetty/build.gradle @@ -4,7 +4,10 @@ plugins { dependencies { implementation libs.jetty.servlet + implementation(libs.jetty.http2.server) + implementation(libs.jetty.alpn.server) testImplementation libs.bcpkix + testImplementation(libs.jetty.alpn.conscrypt.server) testCompileOnly(mnValidation.micronaut.validation.processor) testAnnotationProcessor(mnValidation.micronaut.validation.processor) testAnnotationProcessor(projects.micronautServletProcessor) diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index b8454890a..4dcef97b6 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -40,7 +40,10 @@ import java.util.List; import java.util.stream.Stream; import java.util.concurrent.ExecutorService; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; @@ -146,14 +149,36 @@ protected Server jettyServer( https = newHttpsConnector(server, sslConfiguration, jettySslConfiguration); } - final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(jettyConfiguration.getHttpConfiguration())); - http.setPort(port); - http.setHost(host); + final ServerConnector http = newHttpConnector(server, host, port); configureConnectors(server, http, https); return server; } + /** + * Create the HTTP connector. + * @param server The server + * @param host The host + * @param port The port + * @return The server connector. + */ + protected @NonNull ServerConnector newHttpConnector(@NonNull Server server, @NonNull String host, @NonNull Integer port) { + HttpConfiguration httpConfig = jettyConfiguration.getHttpConfiguration(); + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + HttpServerConfiguration serverConfiguration = getServerConfiguration(); + final ServerConnector http; + if (serverConfiguration.getHttpVersion() == io.micronaut.http.HttpVersion.HTTP_2_0) { + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + http = new ServerConnector(server, http11, h2c); + } else { + http = new ServerConnector(server, http11); + } + + http.setPort(port); + http.setHost(host); + return http; + } + /** * Create the HTTPS connector. * @@ -216,10 +241,29 @@ protected Server jettyServer( HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); httpsConfig.addCustomizer(jettySslConfiguration); - https = new ServerConnector(server, - new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), - new HttpConnectionFactory(httpsConfig) - ); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpsConfig); + + if (getServerConfiguration().getHttpVersion() == io.micronaut.http.HttpVersion.HTTP_2_0) { + // The ConnectionFactory for HTTP/2. + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig); + // The ALPN ConnectionFactory. + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + // The default protocol to use in case there is no negotiation. + alpn.setDefaultProtocol(http11.getProtocol()); + // The ConnectionFactory for TLS. + SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + // The ServerConnector instance. + https = new ServerConnector(server, tls, alpn, h2, http11); + } else { + SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()); + https = new ServerConnector(server, + sslConnectionFactory, + http11 + ); + } + https.setPort(securePort); return https; } diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2OverPlaintextSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2OverPlaintextSpec.groovy new file mode 100644 index 000000000..4e9a2c8b9 --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2OverPlaintextSpec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.servlet.jetty + +import groovy.transform.EqualsAndHashCode +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.HttpVersion +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.HttpVersionSelection +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +@Property(name = 'spec.name', value = 'JettyHttp2OverPlaintextSpec') +@Property(name = "micronaut.server.http-version", value = "2.0") +class JettyHttp2OverPlaintextSpec extends Specification { + @Inject + @Client( + value = "/", + alpnModes = HttpVersionSelection.ALPN_HTTP_2, + plaintextMode = HttpVersionSelection.PlaintextMode.H2C) + HttpClient client + + void "test simple post request with JSON over h2c"() { + given: + def book = new Book(title: "The Stand", pages: 1000) + + when: + HttpResponse response = client.toBlocking().exchange( + HttpRequest.POST("/post/simple", book) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header("X-My-Header", "Foo"), + Book + ) + Optional body = response.getBody() + + then: + response.status == HttpStatus.OK + response.contentType.get() == MediaType.APPLICATION_JSON_TYPE + response.contentLength == 34 + body.isPresent() + body.get() instanceof Book + body.get() == book + } + + @Introspected + @EqualsAndHashCode + static class Book { + String title + Integer pages + } + + @Requires(property = 'spec.name', value = 'JettyHttp2OverPlaintextSpec') + @Controller('/post') + static class PostController { + + @Post('/simple') + Book simple(HttpRequest request, @Body Book book, @Header String contentType, @Header long contentLength, @Header accept, @Header('X-My-Header') custom) { + assert request.httpVersion == HttpVersion.HTTP_2_0 + assert contentType == MediaType.APPLICATION_JSON + assert contentLength == 34 + assert accept == MediaType.APPLICATION_JSON + assert custom == 'Foo' + return book + } + } +} diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy new file mode 100644 index 000000000..445bacc11 --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.servlet.jetty + +import groovy.transform.EqualsAndHashCode +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.* +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.HttpVersionSelection +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.PendingFeature +import spock.lang.Specification +import spock.util.environment.OperatingSystem + +@MicronautTest +@Property(name = 'spec.name', value = 'JettyHttp2PostSpec') +@Property(name = "micronaut.server.http-version", value = "2.0") +@Property(name = "micronaut.ssl.enabled", value = "true") +@Property(name = "micronaut.ssl.build-self-signed", value = "true") +class JettyHttp2Spec extends Specification { + @Inject + @Client( + value = "/", + alpnModes = HttpVersionSelection.ALPN_HTTP_2) + HttpClient client + + @PendingFeature(reason = "Conscript configuration not supported") + void "test simple post request with JSON over http/2"() { + given: + def book = new Book(title: "The Stand", pages: 1000) + + when: + HttpResponse response = client.toBlocking().exchange( + HttpRequest.POST("/post/simple", book) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header("X-My-Header", "Foo"), + Book + ) + Optional body = response.getBody() + + then: + response.status == HttpStatus.OK + response.contentType.get() == MediaType.APPLICATION_JSON_TYPE + response.contentLength == 34 + body.isPresent() + body.get() instanceof Book + body.get() == book + } + + @Introspected + @EqualsAndHashCode + static class Book { + String title + Integer pages + } + + @Requires(property = 'spec.name', value = 'JettyHttp2PostSpec') + @Controller('/post') + static class PostController { + + @Post('/simple') + Book simple(HttpRequest request, @Body Book book, @Header String contentType, @Header long contentLength, @Header accept, @Header('X-My-Header') custom) { + assert request.httpVersion == HttpVersion.HTTP_2_0 + assert contentType == MediaType.APPLICATION_JSON + assert contentLength == 34 + assert accept == MediaType.APPLICATION_JSON + assert custom == 'Foo' + return book + } + } +} diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index ef9997307..2563d69e0 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpVersion; import io.micronaut.inject.qualifiers.Qualifiers; import jakarta.inject.Named; import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; @@ -41,6 +42,7 @@ import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http2.Http2Protocol; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; @@ -144,7 +146,10 @@ protected void configureServletInitializer(Context context, MicronautServletInit */ protected void configureConnectors(@NonNull Tomcat tomcat, @NonNull Connector httpConnector, @Nullable Connector httpsConnector) { TomcatConfiguration serverConfiguration = getServerConfiguration(); - + HttpVersion httpVersion = getServerConfiguration().getHttpVersion(); + if (httpVersion == HttpVersion.HTTP_2_0) { + httpConnector.addUpgradeProtocol(new Http2Protocol()); + } if (httpsConnector != null) { tomcat.getService().addConnector(httpsConnector); if (serverConfiguration.isDualProtocol()) { diff --git a/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TonmcatHttp2OverPlaintextSpec.groovy b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TonmcatHttp2OverPlaintextSpec.groovy new file mode 100644 index 000000000..55504b24c --- /dev/null +++ b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TonmcatHttp2OverPlaintextSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.servlet.tomcat + +import groovy.transform.EqualsAndHashCode +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.* +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.HttpVersionSelection +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +@Property(name = 'spec.name', value = 'TonmcatHttp2OverPlaintextSpec') +@Property(name = "micronaut.server.http-version", value = "2.0") +class TonmcatHttp2OverPlaintextSpec extends Specification { + @Inject + @Client( + value = "/", + alpnModes = HttpVersionSelection.ALPN_HTTP_2, + plaintextMode = HttpVersionSelection.PlaintextMode.H2C) + HttpClient client + + void "test simple post request with JSON over h2c"() { + given: + def book = new Book(title: "The Stand", pages: 1000) + + when: + HttpResponse response = client.toBlocking().exchange( + HttpRequest.POST("/post/simple", book) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header("X-My-Header", "Foo"), + Book + ) + Optional body = response.getBody() + + then: + response.status == HttpStatus.OK + response.contentType.get() == MediaType.APPLICATION_JSON_TYPE + response.contentLength == 34 + body.isPresent() + body.get() instanceof Book + body.get() == book + } + + @Introspected + @EqualsAndHashCode + static class Book { + String title + Integer pages + } + + @Requires(property = 'spec.name', value = 'TonmcatHttp2OverPlaintextSpec') + @Controller('/post') + static class PostController { + + @Post('/simple') + Book simple(HttpRequest request, @Body Book book, @Header String contentType, @Header long contentLength, @Header accept, @Header('X-My-Header') custom) { + assert request.httpVersion == HttpVersion.HTTP_2_0 + assert contentType == MediaType.APPLICATION_JSON + assert contentLength == 34 + assert accept == MediaType.APPLICATION_JSON + assert custom == 'Foo' + return book + } + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index 18b186fb9..f5c4c3799 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -50,6 +50,7 @@ import io.micronaut.http.server.types.files.SystemFile; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.resource.StaticResourceResolver; +import java.io.EOFException; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -342,136 +343,147 @@ private void encodeResponse(ServletExchange exchange, HttpRequest request, MutableHttpResponse response, Consumer> responsePublisherCallback) { - Object body = response.getBody().orElse(null); + try { + Object body = response.getBody().orElse(null); - if (LOG.isDebugEnabled()) { - LOG.debug("Sending response {}", response.status()); - traceHeaders(response.getHeaders()); - } + if (LOG.isDebugEnabled()) { + LOG.debug("Sending response {}", response.status()); + traceHeaders(response.getHeaders()); + } - AnnotationMetadata routeAnnotationMetadata = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) - .map(AnnotationMetadataProvider::getAnnotationMetadata) - .orElse(AnnotationMetadata.EMPTY_METADATA); + AnnotationMetadata routeAnnotationMetadata = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) + .map(AnnotationMetadataProvider::getAnnotationMetadata) + .orElse(AnnotationMetadata.EMPTY_METADATA); - ServletHttpResponse servletResponse = exchange.getResponse(); - servletResponse.status(response.status(), response.reason()); + ServletHttpResponse servletResponse = exchange.getResponse(); + servletResponse.status(response.status(), response.reason()); - if (body != null) { - Class bodyType = body.getClass(); - ServletResponseEncoder responseEncoder = (ServletResponseEncoder) responseEncoders.get(bodyType); - if (responseEncoder != null) { - if (exchange.getRequest().isAsyncSupported()) { - Flux.from(responseEncoder.encode(exchange, routeAnnotationMetadata, body)) - .subscribe(responsePublisherCallback); - } else { - // NOTE[moss]: blockLast() here *was* subscribe(), but that returns immediately, which was - // sometimes allowing the main response publisher to complete before this responseEncoder - // could fill out the response! Blocking here will ensure that the response is filled out - // before the main response publisher completes. This will be improved later to avoid the block. - Flux.from(responseEncoder.encode(exchange, routeAnnotationMetadata, body)).blockLast(); + if (body != null) { + Class bodyType = body.getClass(); + ServletResponseEncoder responseEncoder = (ServletResponseEncoder) responseEncoders.get(bodyType); + if (responseEncoder != null) { + if (exchange.getRequest().isAsyncSupported()) { + Flux.from(responseEncoder.encode(exchange, routeAnnotationMetadata, body)) + .subscribe(responsePublisherCallback); + } else { + // NOTE[moss]: blockLast() here *was* subscribe(), but that returns immediately, which was + // sometimes allowing the main response publisher to complete before this responseEncoder + // could fill out the response! Blocking here will ensure that the response is filled out + // before the main response publisher completes. This will be improved later to avoid the block. + Flux.from(responseEncoder.encode(exchange, routeAnnotationMetadata, body)).blockLast(); + } + return; } - return; - } - MediaType mediaType = response.getContentType().orElse(null); - if (mediaType == null) { - mediaType = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) - .map(routeInfo -> { - final Produces ann = bodyType.getAnnotation(Produces.class); - if (ann != null) { - final String[] v = ann.value(); - if (ArrayUtils.isNotEmpty(v)) { - return new MediaType(v[0]); + MediaType mediaType = response.getContentType().orElse(null); + if (mediaType == null) { + mediaType = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) + .map(routeInfo -> { + final Produces ann = bodyType.getAnnotation(Produces.class); + if (ann != null) { + final String[] v = ann.value(); + if (ArrayUtils.isNotEmpty(v)) { + return new MediaType(v[0]); + } } - } - return routeExecutor.resolveDefaultResponseContentType(request, routeInfo); - }) - // RouteExecutor will pick json by default, so we do too - .orElse(MediaType.APPLICATION_JSON_TYPE); - response.contentType(mediaType); - } + return routeExecutor.resolveDefaultResponseContentType(request, routeInfo); + }) + // RouteExecutor will pick json by default, so we do too + .orElse(MediaType.APPLICATION_JSON_TYPE); + response.contentType(mediaType); + } - setHeadersFromMetadata(servletResponse, routeAnnotationMetadata, body); - if (Publishers.isConvertibleToPublisher(body)) { - boolean isSingle = Publishers.isSingle(body.getClass()); - Publisher publisher = Publishers.convertPublisher(conversionService, body, Publisher.class); - if (isSingle) { - if (exchange.getRequest().isAsyncSupported()) { - Flux flux = Flux.from(publisher); - flux.next().switchIfEmpty(Mono.just(response)).subscribe(bodyValue -> { - MutableHttpResponse nextResponse; - if (bodyValue instanceof MutableHttpResponse) { - nextResponse = ((MutableHttpResponse) bodyValue); - if (response == nextResponse) { - nextResponse.body(null); + setHeadersFromMetadata(servletResponse, routeAnnotationMetadata, body); + if (Publishers.isConvertibleToPublisher(body)) { + boolean isSingle = Publishers.isSingle(body.getClass()); + Publisher publisher = Publishers.convertPublisher(conversionService, body, Publisher.class); + if (isSingle) { + if (exchange.getRequest().isAsyncSupported()) { + Flux flux = Flux.from(publisher); + flux.next().switchIfEmpty(Mono.just(response)).subscribe(bodyValue -> { + MutableHttpResponse nextResponse; + if (bodyValue instanceof MutableHttpResponse) { + nextResponse = ((MutableHttpResponse) bodyValue); + if (response == nextResponse) { + nextResponse.body(null); + } + } else { + nextResponse = response.body(bodyValue); } - } else { - nextResponse = response.body(bodyValue); - } - // Call encoding again, the body might need to be encoded - encodeResponse(exchange, request, nextResponse, responsePublisherCallback); - }); - return; - } else { - // fallback to blocking - body = Mono.from(publisher).block(); - response.body(body); - } - } else { - // stream case - if (exchange.getRequest().isAsyncSupported()) { - Mono.from(servletResponse.stream(publisher)).subscribe(responsePublisherCallback, throwable -> { - responsePublisherCallback.accept(null); - }); - return; + // Call encoding again, the body might need to be encoded + encodeResponse(exchange, request, nextResponse, responsePublisherCallback); + }); + return; + } else { + // fallback to blocking + body = Mono.from(publisher).block(); + response.body(body); + } } else { - // fallback to blocking - body = Flux.from(publisher).collectList().block(); - servletResponse.body(body); + // stream case + if (exchange.getRequest().isAsyncSupported()) { + Mono.from(servletResponse.stream(publisher)).subscribe(responsePublisherCallback, throwable -> { + responsePublisherCallback.accept(null); + }); + return; + } else { + // fallback to blocking + body = Flux.from(publisher).collectList().block(); + servletResponse.body(body); + } } } - } - if (body instanceof HttpStatus) { - servletResponse.status((HttpStatus) body); - } else if (body instanceof CharSequence) { - if (response.getContentType().isEmpty()) { - response.contentType(MediaType.APPLICATION_JSON); - } - try (BufferedWriter writer = servletResponse.getWriter()) { - writer.write(body.toString()); - writer.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else if (body instanceof byte[] byteArray) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - outputStream.write(byteArray); - outputStream.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else if (body instanceof Writable writable) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - writable.writeTo(outputStream, response.getCharacterEncoding()); - outputStream.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else { - final MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(mediaType, bodyType).orElse(null); - if (codec != null) { + if (body instanceof HttpStatus) { + servletResponse.status((HttpStatus) body); + } else if (body instanceof CharSequence) { + if (response.getContentType().isEmpty()) { + response.contentType(MediaType.APPLICATION_JSON); + } + try (BufferedWriter writer = servletResponse.getWriter()) { + writer.write(body.toString()); + writer.flush(); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } else if (body instanceof byte[] byteArray) { try (OutputStream outputStream = servletResponse.getOutputStream()) { - codec.encode(body, outputStream); + outputStream.write(byteArray); outputStream.flush(); - } catch (Throwable e) { - throw new CodecException("Failed to encode object [" + body + "] to content type [" + mediaType + "]: " + e.getMessage(), e); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } else if (body instanceof Writable writable) { + try (OutputStream outputStream = servletResponse.getOutputStream()) { + writable.writeTo(outputStream, response.getCharacterEncoding()); + outputStream.flush(); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } else { - throw new CodecException("No codec present capable of encoding object [" + body + "] to content type [" + mediaType + "]"); + final MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(mediaType, bodyType).orElse(null); + if (codec != null) { + try (OutputStream outputStream = servletResponse.getOutputStream()) { + codec.encode(body, outputStream); + outputStream.flush(); + } catch (Throwable e) { + if (e instanceof CodecException codecException) { + throw codecException; + } + throw new CodecException("Failed to encode object [" + body + "] to content type [" + mediaType + "]: " + e.getMessage(), e); + } + } else { + throw new CodecException("No codec present capable of encoding object [" + body + "] to content type [" + mediaType + "]"); + } } } + responsePublisherCallback.accept(response); + } catch (CodecException e) { + if (e.getCause() instanceof EOFException) { + // connection dropped, nothing we can do + } else { + throw e; + } } - responsePublisherCallback.accept(response); } private void setHeadersFromMetadata(MutableHttpResponse res, AnnotationMetadata annotationMetadata, Object result) { diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java index 6569a13ef..bb9feb015 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java @@ -34,6 +34,8 @@ import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpParameters; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpVersion; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.codec.MediaTypeCodecRegistry; @@ -78,7 +80,7 @@ import java.util.function.Supplier; /** - * Implementation of {@link io.micronaut.http.HttpRequest} ontop of the Servlet API. + * Implementation of {@link HttpRequest} ontop of the Servlet API. * * @param The body type * @author graemerocher @@ -153,6 +155,15 @@ protected DefaultServletHttpRequest(ConversionService conversionService, }); } + @Override + public HttpVersion getHttpVersion() { + String protocol = getNativeRequest().getProtocol(); + return switch (protocol) { + case "HTTP/2.0" -> HttpVersion.HTTP_2_0; + default -> ServletHttpRequest.super.getHttpVersion(); + }; + } + @Override public ConversionService getConversionService() { return this.conversionService; From cc801ae9dae8d5094ac6da9bf4f6eec0e3636bd7 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 May 2024 12:20:38 +0200 Subject: [PATCH 019/180] Support MessageBodyReader/Writer abstraction in Servlet (#707) Adds support for using `MessageBodyReader` and `MessageBodyWriter` implementations in servlet --- .../servlet/jetty/JettyHttp2Spec.groovy | 1 + .../servlet/http/ServletBodyBinder.java | 159 ++++++++++-------- .../servlet/http/ServletHttpHandler.java | 116 ++++++++----- .../engine/DefaultServletHttpResponse.java | 35 ++-- 4 files changed, 187 insertions(+), 124 deletions(-) diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy index 445bacc11..7a9f12428 100644 --- a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyHttp2Spec.groovy @@ -23,6 +23,7 @@ import spock.util.environment.OperatingSystem @Property(name = "micronaut.server.http-version", value = "2.0") @Property(name = "micronaut.ssl.enabled", value = "true") @Property(name = "micronaut.ssl.build-self-signed", value = "true") +@spock.lang.Requires({ os.family != OperatingSystem.Family.MAC_OS }) class JettyHttp2Spec extends Specification { @Inject @Client( diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java index 01d4e0c0a..3459832ea 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java @@ -23,21 +23,23 @@ import io.micronaut.core.io.IOUtils; import io.micronaut.core.io.Readable; import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpAttributes; 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.body.MessageBodyReader; import io.micronaut.http.codec.CodecException; import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.web.router.RouteInfo; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Array; import java.util.Collections; @@ -84,35 +86,13 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest servletHttpRequest) { if (Readable.class.isAssignableFrom(type)) { - Readable readable = new Readable() { - @Override - public Reader asReader() throws IOException { - return servletHttpRequest.getReader(); - } - - @NonNull - @Override - public InputStream asInputStream() throws IOException { - return servletHttpRequest.getInputStream(); - } - - @Override - public boolean exists() { - return true; - } - - @NonNull - @Override - public String getName() { - return servletHttpRequest.getPath(); - } - }; + Readable readable = new ServletReadable(servletHttpRequest); return () -> (Optional) Optional.of(readable); } if (CharSequence.class.isAssignableFrom(type) && name == null) { - try (InputStream inputStream = servletHttpRequest.getInputStream()) { - final String content = IOUtils.readText(new BufferedReader(new InputStreamReader(inputStream, source.getCharacterEncoding()))); - return () -> (Optional) Optional.of(content); + try (BufferedReader bufferedReader = servletHttpRequest.getReader()) { + String text = IOUtils.readText(bufferedReader); + return () -> (Optional) Optional.of(text); } catch (IOException e) { return new BindingResult() { @Override @@ -137,49 +117,66 @@ public List getConversionErrors() { Optional result = conversionService.convert(servletHttpRequest.getParameters().asMap(), context); return () -> result; } - } - final MediaTypeCodec codec = mediaTypeCodeRegistry - .findCodec(mediaType, type) + } else { + MessageBodyReader messageBodyReader = source.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) + .map(RouteInfo::getMessageBodyReader) .orElse(null); - - if (codec != null) { - try (InputStream inputStream = servletHttpRequest.getInputStream()) { - if (Publishers.isConvertibleToPublisher(type)) { - 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); - final T converted = conversionService.convertRequired(publisher, type); - return () -> Optional.of(converted); + if (name == null && messageBodyReader != null && messageBodyReader.isReadable(context.getArgument(), mediaType)) { + try (InputStream inputStream = servletHttpRequest.getInputStream()) { + Object content = messageBodyReader.read(context.getArgument(), mediaType, source.getHeaders(), inputStream); + if (content != null && servletHttpRequest instanceof ParsedBodyHolder parsedBody) { + parsedBody.setParsedBody(content); } - final Argument> containerType = Argument.listOf(typeArg.getType()); - T content = (T) codec.decode(containerType, inputStream); - final Publisher publisher = Flux.fromIterable((Iterable) content); - final T converted = conversionService.convertRequired(publisher, type); - return () -> Optional.of(converted); - } - if (type.isAssignableFrom(byte[].class)) { - byte[] content = inputStream.readAllBytes(); - return () -> Optional.of((T) content); - } else if (type.isArray()) { - Class componentType = type.getComponentType(); - List content = (List) codec.decode(Argument.listOf(componentType), inputStream); - Object[] array = content.toArray((Object[]) Array.newInstance(componentType, 0)); - return () -> Optional.of((T) array); - } - T content; - if (name != null) { - var decode = codec.decode(Map.class, inputStream); - content = conversionService.convert(decode.get(name), argument).orElse(null); - } else { - content = codec.decode(argument, inputStream); + return () -> (Optional) Optional.ofNullable(content); + } catch (CodecException | IOException e) { + throw new CodecException("Unable to decode request body: " + e.getMessage(), e); } - if (content != null && servletHttpRequest instanceof ParsedBodyHolder parsedBody) { - parsedBody.setParsedBody(content); + } else { + + final MediaTypeCodec codec = mediaTypeCodeRegistry + .findCodec(mediaType, type) + .orElse(null); + + if (codec != null) { + try (InputStream inputStream = servletHttpRequest.getInputStream()) { + if (Publishers.isConvertibleToPublisher(type)) { + 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); + final T converted = conversionService.convertRequired(publisher, type); + return () -> Optional.of(converted); + } + final Argument> containerType = Argument.listOf(typeArg.getType()); + T content = (T) codec.decode(containerType, inputStream); + final Publisher publisher = Flux.fromIterable((Iterable) content); + final T converted = conversionService.convertRequired(publisher, type); + return () -> Optional.of(converted); + } + if (type.isAssignableFrom(byte[].class)) { + byte[] content = inputStream.readAllBytes(); + return () -> Optional.of((T) content); + } else if (type.isArray()) { + Class componentType = type.getComponentType(); + List content = (List) codec.decode(Argument.listOf(componentType), inputStream); + Object[] array = content.toArray((Object[]) Array.newInstance(componentType, 0)); + return () -> Optional.of((T) array); + } + T content; + if (name != null) { + var decode = codec.decode(Map.class, inputStream); + content = conversionService.convert(decode.get(name), argument).orElse(null); + } else { + content = codec.decode(argument, inputStream); + } + if (content != null && servletHttpRequest instanceof ParsedBodyHolder parsedBody) { + parsedBody.setParsedBody(content); + } + return () -> Optional.of(content); + } catch (CodecException | IOException e) { + throw new CodecException("Unable to decode request body: " + e.getMessage(), e); + } } - return () -> Optional.of(content); - } catch (CodecException | IOException e) { - throw new CodecException("Unable to decode request body: " + e.getMessage(), e); } } @@ -190,4 +187,34 @@ public List getConversionErrors() { private boolean isFormSubmission(MediaType contentType) { return MediaType.APPLICATION_FORM_URLENCODED_TYPE.equals(contentType) || MediaType.MULTIPART_FORM_DATA_TYPE.equals(contentType); } + + private static final class ServletReadable implements Readable { + private final ServletHttpRequest servletHttpRequest; + + public ServletReadable(ServletHttpRequest servletHttpRequest) { + this.servletHttpRequest = servletHttpRequest; + } + + @Override + public Reader asReader() throws IOException { + return servletHttpRequest.getReader(); + } + + @NonNull + @Override + public InputStream asInputStream() throws IOException { + return servletHttpRequest.getInputStream(); + } + + @Override + public boolean exists() { + return true; + } + + @NonNull + @Override + public String getName() { + return servletHttpRequest.getPath(); + } + } } diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index f5c4c3799..88cca1d83 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -26,6 +26,7 @@ import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.Writable; import io.micronaut.core.propagation.PropagatedContext; +import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpHeaders; @@ -36,6 +37,7 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Produces; +import io.micronaut.http.body.MessageBodyWriter; import io.micronaut.http.codec.CodecException; import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; @@ -351,18 +353,26 @@ private void encodeResponse(ServletExchange exchange, traceHeaders(response.getHeaders()); } - AnnotationMetadata routeAnnotationMetadata = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) + @SuppressWarnings("rawtypes") + Optional routeInfoAttribute = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class); + AnnotationMetadata routeAnnotationMetadata = routeInfoAttribute .map(AnnotationMetadataProvider::getAnnotationMetadata) .orElse(AnnotationMetadata.EMPTY_METADATA); - + @SuppressWarnings("unchecked") + Argument bodyArgument = routeInfoAttribute.map(RouteInfo::getResponseBodyType).orElse(null); + boolean isVoid = routeInfoAttribute.map(RouteInfo::isVoid).orElse(false); ServletHttpResponse servletResponse = exchange.getResponse(); servletResponse.status(response.status(), response.reason()); - if (body != null) { + if (body != null && !isVoid) { Class bodyType = body.getClass(); + if (bodyArgument == null || !bodyArgument.isInstance(body)) { + bodyArgument = (Argument) Argument.of(bodyType); + } ServletResponseEncoder responseEncoder = (ServletResponseEncoder) responseEncoders.get(bodyType); + boolean asyncSupported = exchange.getRequest().isAsyncSupported(); if (responseEncoder != null) { - if (exchange.getRequest().isAsyncSupported()) { + if (asyncSupported) { Flux.from(responseEncoder.encode(exchange, routeAnnotationMetadata, body)) .subscribe(responsePublisherCallback); } else { @@ -377,7 +387,7 @@ private void encodeResponse(ServletExchange exchange, MediaType mediaType = response.getContentType().orElse(null); if (mediaType == null) { - mediaType = response.getAttribute(HttpAttributes.ROUTE_INFO, RouteInfo.class) + mediaType = routeInfoAttribute .map(routeInfo -> { final Produces ann = bodyType.getAnnotation(Produces.class); if (ann != null) { @@ -398,7 +408,7 @@ private void encodeResponse(ServletExchange exchange, boolean isSingle = Publishers.isSingle(body.getClass()); Publisher publisher = Publishers.convertPublisher(conversionService, body, Publisher.class); if (isSingle) { - if (exchange.getRequest().isAsyncSupported()) { + if (asyncSupported) { Flux flux = Flux.from(publisher); flux.next().switchIfEmpty(Mono.just(response)).subscribe(bodyValue -> { MutableHttpResponse nextResponse; @@ -421,7 +431,7 @@ private void encodeResponse(ServletExchange exchange, } } else { // stream case - if (exchange.getRequest().isAsyncSupported()) { + if (asyncSupported) { Mono.from(servletResponse.stream(publisher)).subscribe(responsePublisherCallback, throwable -> { responsePublisherCallback.accept(null); }); @@ -433,54 +443,70 @@ private void encodeResponse(ServletExchange exchange, } } } - if (body instanceof HttpStatus) { - servletResponse.status((HttpStatus) body); - } else if (body instanceof CharSequence) { - if (response.getContentType().isEmpty()) { - response.contentType(MediaType.APPLICATION_JSON); - } - try (BufferedWriter writer = servletResponse.getWriter()) { - writer.write(body.toString()); - writer.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else if (body instanceof byte[] byteArray) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - outputStream.write(byteArray); - outputStream.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else if (body instanceof Writable writable) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - writable.writeTo(outputStream, response.getCharacterEncoding()); - outputStream.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } + if (body instanceof HttpStatus httpStatus) { + servletResponse.status(httpStatus); } else { - final MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(mediaType, bodyType).orElse(null); - if (codec != null) { + if (body instanceof CharSequence) { + if (response.getContentType().isEmpty()) { + response.contentType(MediaType.APPLICATION_JSON); + } + try (BufferedWriter writer = servletResponse.getWriter()) { + writer.write(body.toString()); + writer.flush(); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } else if (body instanceof byte[] byteArray) { try (OutputStream outputStream = servletResponse.getOutputStream()) { - codec.encode(body, outputStream); + outputStream.write(byteArray); outputStream.flush(); - } catch (Throwable e) { - if (e instanceof CodecException codecException) { - throw codecException; - } - throw new CodecException("Failed to encode object [" + body + "] to content type [" + mediaType + "]: " + e.getMessage(), e); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } else if (body instanceof Writable writable) { + try (OutputStream outputStream = servletResponse.getOutputStream()) { + writable.writeTo(outputStream, response.getCharacterEncoding()); + outputStream.flush(); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } else { - throw new CodecException("No codec present capable of encoding object [" + body + "] to content type [" + mediaType + "]"); + MessageBodyWriter messageBodyWriter = + routeInfoAttribute.map(RouteInfo::getMessageBodyWriter).orElse(null); + if (messageBodyWriter != null && messageBodyWriter.isWriteable(bodyArgument, mediaType)) { + try (OutputStream outputStream = servletResponse.getOutputStream()) { + messageBodyWriter.writeTo( + bodyArgument, + mediaType, + body, + response.getHeaders(), + outputStream + ); + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } else { + final MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(mediaType, bodyType).orElse(null); + if (codec != null) { + try (OutputStream outputStream = servletResponse.getOutputStream()) { + codec.encode(body, outputStream); + outputStream.flush(); + } catch (Throwable e) { + if (e instanceof CodecException codecException) { + throw codecException; + } + throw new CodecException("Failed to encode object [" + body + "] to content type [" + mediaType + "]: " + e.getMessage(), e); + } + } else { + throw new CodecException("No codec present capable of encoding object [" + body + "] to content type [" + mediaType + "]"); + } + } } } } responsePublisherCallback.accept(response); } catch (CodecException e) { - if (e.getCause() instanceof EOFException) { - // connection dropped, nothing we can do - } else { + if (!(e.getCause() instanceof EOFException)) { throw e; } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java index 4f5f8aa01..1a1c5500a 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java @@ -26,6 +26,8 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpResponseProvider; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpHeaders; @@ -380,21 +382,28 @@ public Optional getBody() { @SuppressWarnings("unchecked") @Override public MutableHttpResponse body(@Nullable T body) { - if (body != null) { - getContentType().orElseGet(() -> { - final Produces ann = body.getClass().getAnnotation(Produces.class); - if (ann != null) { - final String[] v = ann.value(); - if (ArrayUtils.isNotEmpty(v)) { - final MediaType mediaType = new MediaType(v[0]); - contentType(mediaType); - return mediaType; + if (body instanceof HttpResponseProvider responseProvider) { + HttpResponse response = (HttpResponse) responseProvider.getResponse(); + if (response != this && response.body() != null) { + body(response.body()); + } + } else { + if (body != null) { + getContentType().orElseGet(() -> { + final Produces ann = body.getClass().getAnnotation(Produces.class); + if (ann != null) { + final String[] v = ann.value(); + if (ArrayUtils.isNotEmpty(v)) { + final MediaType mediaType = new MediaType(v[0]); + contentType(mediaType); + return mediaType; + } } - } - return null; - }); + return null; + }); + } + this.body = (B) body; } - this.body = (B) body; return (MutableHttpResponse) this; } From 6a915e7b6bdea77cd14901c21ea7d1d7b3ce10e3 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 27 May 2024 15:57:54 +0200 Subject: [PATCH 020/180] Attribute binders for ServletConfig/ServletContext (#708) * Add attribute binders for ServletConfig/ServletContext * Delegate attribute storage to servlet request * Add api module to keep engine module mainly runtime API --- .../jetty/JettyParameterBinding2Spec.groovy | 12 +++ .../servlet/jetty/MyFilterFactory.java | 2 +- .../servlet/jetty/ParametersController.java | 7 ++ servlet-api/build.gradle.kts | 12 +++ .../servlet/api/ServletAttributes.java | 64 ++++++++++++++ .../servlet/api}/annotation/ServletBean.java | 2 +- .../api}/annotation/ServletFilterBean.java | 2 +- servlet-engine/build.gradle | 2 +- .../engine/DefaultMicronautServlet.java | 4 +- .../engine/DefaultServletHttpRequest.java | 86 +++++++++++++------ .../engine/DefaultServletHttpResponse.java | 2 +- .../bind/DefaultServletBinderRegistry.java | 4 + .../engine/bind/ServletConfigBinder.java | 44 ++++++++++ .../engine/bind/ServletContextBinder.java | 44 ++++++++++ settings.gradle | 1 + src/main/docs/guide/servletAnnotation.adoc | 4 +- 16 files changed, 256 insertions(+), 36 deletions(-) create mode 100644 servlet-api/build.gradle.kts create mode 100644 servlet-api/src/main/java/io/micronaut/servlet/api/ServletAttributes.java rename {servlet-engine/src/main/java/io/micronaut/servlet/engine => servlet-api/src/main/java/io/micronaut/servlet/api}/annotation/ServletBean.java (98%) rename {servlet-engine/src/main/java/io/micronaut/servlet/engine => servlet-api/src/main/java/io/micronaut/servlet/api}/annotation/ServletFilterBean.java (98%) create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletConfigBinder.java create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletContextBinder.java diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyParameterBinding2Spec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyParameterBinding2Spec.groovy index ddfe5ce20..7f1711fd3 100644 --- a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyParameterBinding2Spec.groovy +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyParameterBinding2Spec.groovy @@ -88,6 +88,18 @@ class JettyParameterBinding2Spec extends Specification { response.body() == 'Hello text/plain;q=1.0' } + void "test context binding"() { + given: + def request = HttpRequest.GET("/parameters/context") + request.header(HttpHeaders.CONTENT_TYPE, "text/plain;q=1.0") + def response = client.toBlocking().exchange(request, String) + + expect: + response.status() == HttpStatus.OK + response.contentType.get() == MediaType.TEXT_PLAIN_TYPE + response.body() == 'Hello micronaut /' + } + void "test request and response"() { given: def request = HttpRequest.GET("/parameters/reqAndRes") diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java index f779c0c2a..b6454ab59 100644 --- a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java @@ -3,7 +3,7 @@ import io.micronaut.context.annotation.Factory; import io.micronaut.core.annotation.Order; import io.micronaut.core.order.Ordered; -import io.micronaut.servlet.engine.annotation.ServletFilterBean; +import io.micronaut.servlet.api.annotation.ServletFilterBean; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.GenericFilter; diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/ParametersController.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/ParametersController.java index d350bb118..460cce540 100644 --- a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/ParametersController.java +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/ParametersController.java @@ -7,6 +7,8 @@ import io.micronaut.http.annotation.*; import io.micronaut.http.cookie.Cookie; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.*; @@ -34,6 +36,11 @@ String headerValue(@Header(HttpHeaders.CONTENT_TYPE) String contentType) { return "Hello " + contentType; } + @Get(value = "/context", produces = MediaType.TEXT_PLAIN) + String context(ServletConfig config, ServletContext servletContext) { + return "Hello " + config.getServletName() + " " + servletContext.getServletContextName(); + } + @Get("/cookies") io.micronaut.http.HttpResponse cookies(@CookieValue String myCookie) { return io.micronaut.http.HttpResponse.ok(myCookie) diff --git a/servlet-api/build.gradle.kts b/servlet-api/build.gradle.kts new file mode 100644 index 000000000..680e086ad --- /dev/null +++ b/servlet-api/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("io.micronaut.build.internal.servlet.module") +} + +dependencies { + api(projects.micronautServletCore) + api(libs.managed.servlet.api) +} + +micronautBuild { + binaryCompatibility.enabled.set(false) +} diff --git a/servlet-api/src/main/java/io/micronaut/servlet/api/ServletAttributes.java b/servlet-api/src/main/java/io/micronaut/servlet/api/ServletAttributes.java new file mode 100644 index 000000000..72c497514 --- /dev/null +++ b/servlet-api/src/main/java/io/micronaut/servlet/api/ServletAttributes.java @@ -0,0 +1,64 @@ +/* + * 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.servlet.api; + +import io.micronaut.core.annotation.NonNull; + +/** + * Attributes to lookup servlet related types from the request. + * + * @see io.micronaut.http.HttpAttributes + */ +public enum ServletAttributes implements CharSequence { + + /** + * Attribute to lookup the {@link jakarta.servlet.ServletConfig}. + */ + SERVLET_CONFIG("io.micronaut.servlet.SERVLET_CONFIG"), + + /** + * Attribute to lookup the {@link jakarta.servlet.ServletContext}. + */ + SERVLET_CONTEXT("io.micronaut.servlet.SERVLET_CONTEXT"); + + private final String name; + + ServletAttributes(String name) { + this.name = name; + } + + @Override + public int length() { + return name.length(); + } + + @Override + public char charAt(int index) { + return name.charAt(index); + } + + @Override + @NonNull + public CharSequence subSequence(int start, int end) { + return name.substring(start, end); + } + + @Override + @NonNull + public String toString() { + return name; + } +} diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java b/servlet-api/src/main/java/io/micronaut/servlet/api/annotation/ServletBean.java similarity index 98% rename from servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java rename to servlet-api/src/main/java/io/micronaut/servlet/api/annotation/ServletBean.java index f1e973960..f2a9ae270 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletBean.java +++ b/servlet-api/src/main/java/io/micronaut/servlet/api/annotation/ServletBean.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.servlet.engine.annotation; +package io.micronaut.servlet.api.annotation; import io.micronaut.context.annotation.AliasFor; import io.micronaut.context.annotation.Bean; diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java b/servlet-api/src/main/java/io/micronaut/servlet/api/annotation/ServletFilterBean.java similarity index 98% rename from servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java rename to servlet-api/src/main/java/io/micronaut/servlet/api/annotation/ServletFilterBean.java index 9c191347f..09c6f31d2 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/annotation/ServletFilterBean.java +++ b/servlet-api/src/main/java/io/micronaut/servlet/api/annotation/ServletFilterBean.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.servlet.engine.annotation; +package io.micronaut.servlet.api.annotation; import io.micronaut.context.annotation.AliasFor; import io.micronaut.context.annotation.Bean; diff --git a/servlet-engine/build.gradle b/servlet-engine/build.gradle index 9c9cebc99..ee463596c 100644 --- a/servlet-engine/build.gradle +++ b/servlet-engine/build.gradle @@ -6,7 +6,7 @@ dependencies { annotationProcessor mn.micronaut.graal annotationProcessor(projects.micronautServletProcessor) - api(projects.micronautServletCore) + api(projects.micronautServletApi) api libs.managed.servlet.api implementation(mnReactor.micronaut.reactor) diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java index 5f3f99e3e..fe595d862 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultMicronautServlet.java @@ -19,7 +19,7 @@ import io.micronaut.context.ApplicationContextBuilder; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.TypeHint; - +import io.micronaut.servlet.api.ServletAttributes; import jakarta.inject.Inject; import jakarta.servlet.ServletContext; import jakarta.servlet.annotation.WebServlet; @@ -71,6 +71,8 @@ public DefaultMicronautServlet() { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) { if (handler != null) { + req.setAttribute(ServletAttributes.SERVLET_CONFIG.toString(), getServletConfig()); + req.setAttribute(ServletAttributes.SERVLET_CONTEXT.toString(), getServletContext()); handler.service(req, resp); } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java index bb9feb015..039e0b773 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java @@ -22,7 +22,6 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.convert.value.MutableConvertibleValuesMap; import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; @@ -52,12 +51,6 @@ import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.reactivestreams.Subscriber; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Sinks; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -76,8 +69,12 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; +import org.reactivestreams.Subscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; /** * Implementation of {@link HttpRequest} ontop of the Servlet API. @@ -87,15 +84,15 @@ * @since 1.0.0 */ @Internal -public final class DefaultServletHttpRequest extends MutableConvertibleValuesMap implements +public final class DefaultServletHttpRequest implements ServletHttpRequest, - MutableConvertibleValues, ServletExchange, StreamedServletMessage, FullHttpRequest, ParsedBodyHolder { private static final Logger LOG = LoggerFactory.getLogger(DefaultServletHttpRequest.class); + private static final String NULL_KEY = "Attribute key cannot be null"; private final ConversionService conversionService; private final HttpServletRequest delegate; @@ -105,6 +102,7 @@ public final class DefaultServletHttpRequest extends MutableConvertibleValues private final ServletParameters parameters; private final DefaultServletHttpResponse response; private final MediaTypeCodecRegistry codecRegistry; + private final MutableConvertibleValues attributes; private DefaultServletCookies cookies; private Supplier> body; @@ -126,7 +124,7 @@ protected DefaultServletHttpRequest(ConversionService conversionService, HttpServletResponse response, MediaTypeCodecRegistry codecRegistry, BodyBuilder bodyBuilder) { - super(new ConcurrentHashMap<>(), conversionService); + super(); this.conversionService = conversionService; this.delegate = delegate; this.codecRegistry = codecRegistry; @@ -153,6 +151,54 @@ protected DefaultServletHttpRequest(ConversionService conversionService, B built = parsedBody != null ? parsedBody : (B) bodyBuilder.buildBody(this::getInputStream, this); return Optional.ofNullable(built); }); + this.attributes = new MutableConvertibleValues<>() { + + @Override + public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { + Objects.requireNonNull(conversionContext, "Conversion context cannot be null"); + Objects.requireNonNull(name, NULL_KEY); + Object attribute = delegate.getAttribute(name.toString()); + return Optional.ofNullable(attribute) + .flatMap(v -> conversionService.convert(v, conversionContext)); + } + + @Override + public Set names() { + return CollectionUtils.enumerationToSet(delegate.getAttributeNames()); + } + + @Override + public Collection values() { + return names().stream().map(delegate::getAttribute).toList(); + } + + @Override + public MutableConvertibleValues put(CharSequence key, @Nullable Object value) { + Objects.requireNonNull(key, NULL_KEY); + delegate.setAttribute(key.toString(), value); + return this; + } + + @Override + public MutableConvertibleValues remove(CharSequence key) { + Objects.requireNonNull(key, NULL_KEY); + delegate.removeAttribute(key.toString()); + return this; + } + + @Override + public MutableConvertibleValues clear() { + names().forEach(delegate::removeAttribute); + return this; + } + }; + } + + /** + * @return The conversion service. + */ + public ConversionService getConversionService() { + return conversionService; } @Override @@ -164,11 +210,6 @@ public HttpVersion getHttpVersion() { }; } - @Override - public ConversionService getConversionService() { - return this.conversionService; - } - /** * @return The codec registry. */ @@ -334,7 +375,7 @@ public HttpHeaders getHeaders() { @NonNull @Override public MutableConvertibleValues getAttributes() { - return this; + return this.attributes; } @Override @@ -348,17 +389,6 @@ public Optional getBody() { return this.body.get(); } - @Override - public MutableConvertibleValues put(CharSequence key, @Nullable Object value) { - String name = Objects.requireNonNull(key, "Key cannot be null").toString(); - if (value == null) { - super.remove(name); - } else { - super.put(name, value); - } - return this; - } - @SuppressWarnings("unchecked") @Override public ServletHttpRequest getRequest() { diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java index 1a1c5500a..8116a9719 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpResponse.java @@ -370,7 +370,7 @@ public MutableHttpHeaders getHeaders() { @NonNull @Override public MutableConvertibleValues getAttributes() { - return request; + return request.getAttributes(); } @NonNull diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/DefaultServletBinderRegistry.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/DefaultServletBinderRegistry.java index b906ce17d..e8bce6421 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/DefaultServletBinderRegistry.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/DefaultServletBinderRegistry.java @@ -34,6 +34,8 @@ import io.micronaut.servlet.http.ServletBodyBinder; import io.micronaut.servlet.http.StreamedServletMessage; import jakarta.inject.Singleton; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; import org.reactivestreams.Processor; import reactor.core.publisher.Flux; @@ -80,6 +82,8 @@ public DefaultServletBinderRegistry( super(mediaTypeCodecRegistry, conversionService, binders, defaultBodyAnnotationBinder); byType.put(HttpServletRequest.class, new ServletRequestBinder()); byType.put(HttpServletResponse.class, new ServletResponseBinder()); + byType.put(ServletConfig.class, new ServletConfigBinder()); + byType.put(ServletContext.class, new ServletContextBinder()); byType.put(CompletedPart.class, new CompletedPartRequestArgumentBinder()); byAnnotation.put(Part.class, new ServletPartBinder<>(mediaTypeCodecRegistry)); } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletConfigBinder.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletConfigBinder.java new file mode 100644 index 000000000..5ac79c9c8 --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletConfigBinder.java @@ -0,0 +1,44 @@ +/* + * 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.servlet.engine.bind; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.servlet.api.ServletAttributes; +import jakarta.servlet.ServletConfig; + +/** + * Argument binder for the servlet config. + */ +@Internal +final class ServletConfigBinder implements TypedRequestArgumentBinder { + + public static final @NonNull Argument TYPE = Argument.of(ServletConfig.class); + + @Override + public Argument argumentType() { + return TYPE; + } + + @Override + public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + return () -> source.getAttribute(ServletAttributes.SERVLET_CONFIG, ServletConfig.class); + } +} diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletContextBinder.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletContextBinder.java new file mode 100644 index 000000000..9e09b949e --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/bind/ServletContextBinder.java @@ -0,0 +1,44 @@ +/* + * 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.servlet.engine.bind; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.servlet.api.ServletAttributes; +import jakarta.servlet.ServletContext; + +/** + * Argument binder for the servlet context. + */ +@Internal +final class ServletContextBinder implements TypedRequestArgumentBinder { + + public static final @NonNull Argument TYPE = Argument.of(ServletContext.class); + + @Override + public Argument argumentType() { + return TYPE; + } + + @Override + public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + return () -> source.getAttribute(ServletAttributes.SERVLET_CONTEXT, ServletContext.class); + } +} diff --git a/settings.gradle b/settings.gradle index f237909f9..d8359c338 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,7 @@ micronautBuild { include 'servlet-bom' include 'servlet-processor' include 'servlet-core' +include 'servlet-api' include 'servlet-engine' include 'http-server-jetty' include 'http-server-undertow' diff --git a/src/main/docs/guide/servletAnnotation.adoc b/src/main/docs/guide/servletAnnotation.adoc index e68b8fa85..e9796a81a 100644 --- a/src/main/docs/guide/servletAnnotation.adoc +++ b/src/main/docs/guide/servletAnnotation.adoc @@ -14,5 +14,5 @@ NOTE: Using `HIGHEST_PRECEDENCE` will prevent any other filter running before yo In addition, you can use the following annotations on methods of https://docs.micronaut.io/latest/guide/#factories[@Factory beans] to instantiate servlets and filters and register them: -* ann:servlet.engine.annotation.ServletBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webservlet[@WebServlet] but can be applied to a method of a factory to Register a new servlet. -* ann:servlet.engine.annotation.ServletFilterBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webfilter[@WebFilter] but can be applied to a method of a factory to Register a new filter. +* ann:servlet.api.annotation.ServletBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webservlet[@WebServlet] but can be applied to a method of a factory to Register a new servlet. +* ann:servlet.api.annotation.ServletFilterBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webfilter[@WebFilter] but can be applied to a method of a factory to Register a new filter. From 0863b28d24dd216df69e0ba1e74260b3eb0760ad Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 28 May 2024 08:42:56 +0200 Subject: [PATCH 021/180] Update common files (#710) --- .github/workflows/central-sync.yml | 2 +- .github/workflows/gradle.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/central-sync.yml b/.github/workflows/central-sync.yml index dd1f3514a..23b80ba2e 100644 --- a/.github/workflows/central-sync.yml +++ b/.github/workflows/central-sync.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v4 with: ref: v${{ github.event.inputs.release_version }} - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK uses: actions/setup-java@v4 with: diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 78142de20..f8c8ad2db 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -52,7 +52,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.3.2 + uses: gradle/gradle-build-action@v3.3.1 - name: "❓ Optional setup step" run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a4bd1cb3..065ee5620 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 with: token: ${{ secrets.GH_TOKEN }} - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK uses: actions/setup-java@v4 with: From 741543d79f82a820f5c81e71f4d3167873fad49a Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 28 May 2024 10:29:03 +0200 Subject: [PATCH 022/180] allow registering other servlet container initializers (#711) --- .../micronaut/servlet/jetty/JettyFactory.java | 19 ++++--- .../servlet/tomcat/TomcatFactory.java | 36 ++++++++----- .../servlet/undertow/UndertowFactory.java | 53 +++++++++++-------- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index 4dcef97b6..31f8aefe7 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -32,11 +32,12 @@ import io.micronaut.scheduling.LoomSupport; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.servlet.engine.MicronautServletConfiguration; -import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; import jakarta.inject.Singleton; +import jakarta.servlet.ServletContainerInitializer; import java.io.IOException; +import java.util.Collection; import java.util.List; import java.util.stream.Stream; import java.util.concurrent.ExecutorService; @@ -113,7 +114,7 @@ protected Server jettyServer( applicationContext, configuration, jettySslConfiguration, - applicationContext.getBean(MicronautServletInitializer.class) + applicationContext.getBeansOfType(ServletContainerInitializer.class) ); } @@ -123,7 +124,7 @@ protected Server jettyServer( * @param applicationContext This application context * @param configuration The servlet configuration * @param jettySslConfiguration The Jetty SSL config - * @param micronautServletInitializer The micronaut servlet initializer + * @param servletContainerInitializers The micronaut servlet initializer * @return The Jetty server bean */ @Singleton @@ -132,7 +133,7 @@ protected Server jettyServer( ApplicationContext applicationContext, MicronautServletConfiguration configuration, JettyConfiguration.JettySslConfiguration jettySslConfiguration, - MicronautServletInitializer micronautServletInitializer + Collection servletContainerInitializers ) { final String host = getConfiguredHost(); final Integer port = getConfiguredPort(); @@ -141,7 +142,7 @@ protected Server jettyServer( Server server = newServer(applicationContext, configuration); final ServletContextHandler contextHandler = newJettyContext(server, contextPath); - configureServletInitializer(server, contextHandler, micronautServletInitializer); + configureServletInitializer(server, contextHandler, servletContainerInitializers); final SslConfiguration sslConfiguration = getSslConfiguration(); ServerConnector https = null; @@ -273,10 +274,12 @@ protected Server jettyServer( * * @param server The server * @param contextHandler The context handler - * @param micronautServletInitializer The initializer + * @param servletContainerInitializers The servlet initializers */ - protected void configureServletInitializer(Server server, ServletContextHandler contextHandler, MicronautServletInitializer micronautServletInitializer) { - contextHandler.addServletContainerInitializer(micronautServletInitializer); + protected void configureServletInitializer(Server server, ServletContextHandler contextHandler, Collection servletContainerInitializers) { + for (ServletContainerInitializer servletContainerInitializer : servletContainerInitializers) { + contextHandler.addServletContainerInitializer(servletContainerInitializer); + } List resourceHandlers = Stream.concat( getStaticResourceConfigurations().stream().map(this::toHandler), diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 2563d69e0..3559b4577 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -23,7 +23,9 @@ import io.micronaut.inject.qualifiers.Qualifiers; import jakarta.inject.Named; import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; +import jakarta.servlet.ServletContainerInitializer; import java.io.File; +import java.util.Collection; import java.util.List; import io.micronaut.context.ApplicationContext; @@ -92,7 +94,7 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration connector, getApplicationContext().getBean(Connector.class, Qualifiers.byName(HTTPS)), configuration, - getApplicationContext().getBean(MicronautServletInitializer.class)); + getApplicationContext().getBeansOfType(ServletContainerInitializer.class)); } /** @@ -101,7 +103,7 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration * @param connector The connector * @param httpsConnector The HTTPS connectors * @param configuration The servlet configuration - * @param servletInitializer The servlet initializer + * @param servletInitializers The servlet initializer * @return The Tomcat server */ @Singleton @@ -110,31 +112,39 @@ protected Tomcat tomcatServer( Connector connector, @Named(HTTPS) @Nullable Connector httpsConnector, MicronautServletConfiguration configuration, - MicronautServletInitializer servletInitializer) { + Collection servletInitializers) { configuration.setAsyncFileServingEnabled(false); Tomcat tomcat = newTomcat(); final Context context = newTomcatContext(tomcat); - configureServletInitializer(context, servletInitializer); + configureServletInitializer(context, servletInitializers); configureConnectors(tomcat, connector, httpsConnector); return tomcat; } /** - * Configure the Micronaut servlet intializer. + * Configure the Micronaut servlet initializer. * * @param context The context - * @param servletInitializer The intializer + * @param servletInitializers The initializers */ - protected void configureServletInitializer(Context context, MicronautServletInitializer servletInitializer) { - getStaticResourceConfigurations().forEach(config -> - servletInitializer.addMicronautServletMapping(config.getMapping()) - ); - context.addServletContainerInitializer( - servletInitializer, Set.of(DefaultMicronautServlet.class) - ); + protected void configureServletInitializer(Context context, Collection servletInitializers) { + for (ServletContainerInitializer servletInitializer : servletInitializers) { + if (servletInitializer instanceof MicronautServletInitializer micronautServletInitializer) { + getStaticResourceConfigurations().forEach(config -> + micronautServletInitializer.addMicronautServletMapping(config.getMapping()) + ); + context.addServletContainerInitializer( + servletInitializer, Set.of(DefaultMicronautServlet.class) + ); + } else { + context.addServletContainerInitializer( + servletInitializer, Set.of() + ); + } + } } /** diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index c901dad1f..366d1ff00 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -39,6 +39,7 @@ import jakarta.inject.Singleton; import jakarta.servlet.ServletContainerInitializer; import jakarta.servlet.ServletException; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -211,46 +212,54 @@ protected Undertow undertowServer(Undertow.Builder builder) { * * @param servletConfiguration The servlet configuration. * @return The deployment info - * @deprecated Use {@link #deploymentInfo(MicronautServletConfiguration, MicronautServletInitializer)} + * @deprecated Use {@link ##deploymentInfo(MicronautServletConfiguration, Collection)} */ @Deprecated(forRemoval = true, since = "4.8.0") protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration) { - return deploymentInfo(servletConfiguration, getApplicationContext().getBean(MicronautServletInitializer.class)); + return deploymentInfo(servletConfiguration, getApplicationContext().getBeansOfType(ServletContainerInitializer.class)); } /** * The deployment info bean. * * @param servletConfiguration The servlet configuration. - * @param servletInitializer The servlet initializer + * @param servletInitializers The servlet initializer * @return The deployment info */ @Singleton @Primary - protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration, MicronautServletInitializer servletInitializer) { + protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration, Collection servletInitializers) { final String cp = getContextPath(); - getStaticResourceConfigurations().forEach(config -> { - servletInitializer.addMicronautServletMapping(config.getMapping()); - }); - return Servlets.deployment() - .setDeploymentName(servletConfiguration.getName()) - .setClassLoader(getEnvironment().getClassLoader()) - .setContextPath(cp) + for (ServletContainerInitializer servletInitializer : servletInitializers) { + if (servletInitializer instanceof MicronautServletInitializer micronautServletInitializer) { + getStaticResourceConfigurations().forEach(config -> { + micronautServletInitializer.addMicronautServletMapping(config.getMapping()); + }); + } + } + DeploymentInfo deploymentInfo = Servlets.deployment() + .setDeploymentName(servletConfiguration.getName()) + .setClassLoader(getEnvironment().getClassLoader()) + .setContextPath(cp); + for (ServletContainerInitializer servletInitializer : servletInitializers) { + deploymentInfo .addServletContainerInitializer(new ServletContainerInitializerInfo( - MicronautServletInitializer.class, - () -> new InstanceHandle<>() { - @Override - public ServletContainerInitializer getInstance() { - return servletInitializer; - } + servletInitializer.getClass(), + () -> new InstanceHandle<>() { + @Override + public ServletContainerInitializer getInstance() { + return servletInitializer; + } - @Override - public void release() { + @Override + public void release() { - } - }, - Set.of(MicronautServletInitializer.class) + } + }, + Set.of(servletInitializer.getClass()) )); + } + return deploymentInfo; } } From 38d0703b6d1bc573dfd653a45130316897daeebc Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 28 May 2024 12:10:21 +0200 Subject: [PATCH 023/180] support management port in servlet (#712) Fixes #616 --- .../micronaut/servlet/jetty/JettyFactory.java | 31 +++++++++- .../micronaut/servlet/jetty/JettyServer.java | 6 +- .../jetty/JettyManagementPortSpec.groovy | 23 ++++++-- .../servlet/tomcat/TomcatFactory.java | 57 ++++++++++++++++++- .../servlet/tomcat/TomcatServer.java | 3 +- .../tomcat/TomcatManagementPortSpec.groovy | 28 ++++++--- .../servlet/undertow/UndertowFactory.java | 29 ++++++++++ .../servlet/undertow/UndertowServer.java | 11 ++-- .../UndertowManagementPortSpec.groovy | 28 ++++++--- .../engine/DefaultServletHttpRequest.java | 21 ++++++- src/main/docs/guide/knownIssues.adoc | 11 ---- 11 files changed, 204 insertions(+), 44 deletions(-) diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index 31f8aefe7..c110c3474 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -25,6 +25,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.ResourceResolver; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.server.HttpServerConfiguration; import io.micronaut.http.ssl.ClientAuthentication; import io.micronaut.http.ssl.SslConfiguration; @@ -34,17 +35,20 @@ import io.micronaut.servlet.engine.MicronautServletConfiguration; import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; +import io.micronaut.web.router.Router; import jakarta.inject.Singleton; import jakarta.servlet.ServletContainerInitializer; import java.io.IOException; import java.util.Collection; import java.util.List; -import java.util.stream.Stream; +import java.util.Set; import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; @@ -71,6 +75,7 @@ public class JettyFactory extends ServletServerFactory { public static final String RESOURCE_BASE = "resourceBase"; private final JettyConfiguration jettyConfiguration; + private final Router router; /** * Default constructor. @@ -95,6 +100,7 @@ public JettyFactory( staticResourceConfigurations ); this.jettyConfiguration = serverConfiguration; + this.router = applicationContext.findBean(Router.class).orElse(null); } /** @@ -270,7 +276,7 @@ protected Server jettyServer( } /** - * Configures the servlet initializer + * Configures the servlet initializer. * * @param server The server * @param contextHandler The context handler @@ -315,8 +321,29 @@ protected void configureConnectors(@NonNull Server server, @NonNull ServerConnec if (serverConfiguration.isDualProtocol()) { server.addConnector(http); } + applyAdditionalPorts(server, https); } else { server.addConnector(http); + applyAdditionalPorts(server, http); + } + } + + private void applyAdditionalPorts(Server server, ServerConnector serverConnector) { + if (router != null) { + Set exposedPorts = router.getExposedPorts(); + if (CollectionUtils.isNotEmpty(exposedPorts)) { + for (Integer exposedPort : exposedPorts) { + if (!exposedPort.equals(serverConnector.getLocalPort())) { + ServerConnector connector = new ServerConnector( + server, + serverConnector.getConnectionFactories().toArray(ConnectionFactory[]::new) + ); + connector.setPort(exposedPort); + connector.setHost(getConfiguredHost()); + server.addConnector(connector); + } + } + } } } diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java index c35660683..e03e52557 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyServer.java @@ -19,12 +19,11 @@ import io.micronaut.http.server.exceptions.HttpServerException; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.servlet.engine.server.AbstractServletServer; -import org.eclipse.jetty.server.Server; - import jakarta.inject.Singleton; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import org.eclipse.jetty.server.Server; /** * An implementation of the {@link io.micronaut.runtime.server.EmbeddedServer} interface for Jetty. @@ -51,7 +50,8 @@ public JettyServer( @Override protected void startServer() throws Exception { - getServer().start(); + Server server = getServer(); + server.start(); } @Override diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyManagementPortSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyManagementPortSpec.groovy index cb9f6ff7c..a14827299 100644 --- a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyManagementPortSpec.groovy +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyManagementPortSpec.groovy @@ -4,10 +4,12 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.core.io.socket.SocketUtils import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.client.BlockingHttpClient import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.runtime.server.EmbeddedServer import io.netty.handler.ssl.util.SelfSignedCertificate import spock.lang.Issue @@ -59,7 +61,6 @@ class JettyManagementPortSpec extends Specification { ] } - @PendingFeature def 'management port can be configured different to main port'() { given: def port = SocketUtils.findAvailableTcpPort() @@ -73,19 +74,26 @@ class JettyManagementPortSpec extends Specification { when: def mainResponse = mainClient.exchange('/management-port', String) - def healthResponse = mainClient.exchange('/health', String) + def healthResponse = managementClient.exchange('/health', String) then: mainResponse.body() == 'Hello world' healthResponse.body() == '{"status":"UP"}' + when: + mainClient.exchange('/health', String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.NOT_FOUND + + cleanup: mainClient.close() managementClient.close() server.stop() } - @PendingFeature def 'management port can be configured different to main port and uses ssl if also configured'() { given: def port = SocketUtils.findAvailableTcpPort() @@ -99,12 +107,19 @@ class JettyManagementPortSpec extends Specification { when: def mainResponse = mainClient.exchange('/management-port', String) - def healthResponse = mainClient.exchange('/health', String) + def healthResponse = managementClient.exchange('/health', String) then: mainResponse.body() == 'Hello world' healthResponse.body() == '{"status":"UP"}' + when: + mainClient.exchange('/health', String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.NOT_FOUND + cleanup: mainClient.close() managementClient.close() diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 3559b4577..7b4a2d479 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -18,9 +18,11 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpVersion; import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.web.router.Router; import jakarta.inject.Named; import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; import jakarta.servlet.ServletContainerInitializer; @@ -44,6 +46,7 @@ import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http2.Http2Protocol; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; @@ -58,6 +61,8 @@ public class TomcatFactory extends ServletServerFactory { private static final String HTTPS = "HTTPS"; + private static final String CLIENT_AUTH = "clientAuth"; + private final Router router; /** * Default constructor. @@ -75,6 +80,7 @@ protected TomcatFactory( ApplicationContext applicationContext, List staticResourceConfigurations) { super(resourceResolver, serverConfiguration, sslConfiguration, applicationContext, staticResourceConfigurations); + this.router = applicationContext.findBean(Router.class).orElse(null); } @Override @@ -165,11 +171,58 @@ protected void configureConnectors(@NonNull Tomcat tomcat, @NonNull Connector ht if (serverConfiguration.isDualProtocol()) { tomcat.getService().addConnector(httpConnector); } + applyAdditionalPorts(tomcat, httpsConnector); } else { tomcat.setConnector(httpConnector); + applyAdditionalPorts(tomcat, httpConnector); } } + private void applyAdditionalPorts(Tomcat server, Connector serverConnector) { + if (router != null) { + Set exposedPorts = router.getExposedPorts(); + if (CollectionUtils.isNotEmpty(exposedPorts)) { + for (Integer exposedPort : exposedPorts) { + if (!exposedPort.equals(serverConnector.getLocalPort())) { + Connector newConnector = cloneConnectorSettings(serverConnector); + newConnector.setPort(exposedPort); + server.getService().addConnector(newConnector); + } + } + } + } + } + + private static Connector cloneConnectorSettings(Connector serverConnector) { + Connector newConnector = new Connector(serverConnector.getProtocol()); + ProtocolHandler protocolHandler = serverConnector.getProtocolHandler(); + SSLHostConfig[] sslHostConfigs = protocolHandler.findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + newConnector.addSslHostConfig(sslHostConfig); + newConnector.setSecure(true); + newConnector.setScheme("https"); + newConnector.setProperty(CLIENT_AUTH, "false"); + newConnector.setProperty("sslProtocol", "TLS"); + newConnector.setProperty("SSLEnabled", "true"); + } + newConnector.setAllowBackslash(serverConnector.getAllowBackslash()); + newConnector.setAllowTrace(serverConnector.getAllowTrace()); + newConnector.setAsyncTimeout(serverConnector.getAsyncTimeout()); + newConnector.setDiscardFacades(serverConnector.getDiscardFacades()); + newConnector.setEnableLookups(serverConnector.getEnableLookups()); + newConnector.setSecure(serverConnector.getSecure()); + newConnector.setScheme(serverConnector.getScheme()); + newConnector.setEnforceEncodingInGetWriter(serverConnector.getEnforceEncodingInGetWriter()); + newConnector.setMaxCookieCount(serverConnector.getMaxCookieCount()); + newConnector.setMaxPostSize(serverConnector.getMaxPostSize()); + newConnector.setMaxParameterCount(serverConnector.getMaxParameterCount()); + newConnector.setMaxSavePostSize(serverConnector.getMaxSavePostSize()); + newConnector.setParseBodyMethods(serverConnector.getParseBodyMethods()); + newConnector.setRejectSuspiciousURIs(serverConnector.getRejectSuspiciousURIs()); + newConnector.setUseIPVHosts(serverConnector.getUseIPVHosts()); + return newConnector; + } + /** * Create a new context. * @@ -235,14 +288,14 @@ protected Connector sslConnector(SslConfiguration sslConfiguration) { httpsConnector.setPort(sslPort); httpsConnector.setSecure(true); httpsConnector.setScheme("https"); - httpsConnector.setProperty("clientAuth", "false"); + httpsConnector.setProperty(CLIENT_AUTH, "false"); httpsConnector.setProperty("sslProtocol", protocol); httpsConnector.setProperty("SSLEnabled", "true"); sslConfiguration.getCiphers().ifPresent(cyphers -> sslHostConfig.setCiphers(String.join(",", cyphers)) ); sslConfiguration.getClientAuthentication().ifPresent(ca -> - httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true") + httpsConnector.setProperty(CLIENT_AUTH, ca == ClientAuthentication.WANT ? "want" : "true") ); diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatServer.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatServer.java index 799d565a4..bede41581 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatServer.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatServer.java @@ -57,7 +57,8 @@ public TomcatServer( @Override protected void startServer() throws Exception { if (running.compareAndSet(false, true)) { - getServer().start(); + Tomcat server = getServer(); + server.start(); } } diff --git a/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatManagementPortSpec.groovy b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatManagementPortSpec.groovy index cdc6bee75..bb31bb153 100644 --- a/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatManagementPortSpec.groovy +++ b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatManagementPortSpec.groovy @@ -4,10 +4,12 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.core.io.socket.SocketUtils import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.client.BlockingHttpClient import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.runtime.server.EmbeddedServer import io.netty.handler.ssl.util.SelfSignedCertificate import spock.lang.Issue @@ -59,12 +61,11 @@ class TomcatManagementPortSpec extends Specification { ] } - @PendingFeature def 'management port can be configured different to main port'() { given: def port = SocketUtils.findAvailableTcpPort() EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name' : 'JettyManagementPortSpec', + 'spec.name' : 'TomcatManagementPortSpec', 'endpoints.all.enabled': true, 'endpoints.all.port' : port, ]) @@ -73,24 +74,30 @@ class TomcatManagementPortSpec extends Specification { when: def mainResponse = mainClient.exchange('/management-port', String) - def healthResponse = mainClient.exchange('/health', String) + def healthResponse = managementClient.exchange('/health', String) then: mainResponse.body() == 'Hello world' healthResponse.body() == '{"status":"UP"}' + when: + mainClient.exchange('/health', String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.NOT_FOUND + cleanup: mainClient.close() managementClient.close() server.stop() } - @PendingFeature def 'management port can be configured different to main port and uses ssl if also configured'() { given: def port = SocketUtils.findAvailableTcpPort() EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name' : 'JettyManagementPortSpec', + 'spec.name' : 'TomcatManagementPortSpec', 'endpoints.all.enabled': true, 'endpoints.all.port' : port, ] + sslConfig()) @@ -99,12 +106,19 @@ class TomcatManagementPortSpec extends Specification { when: def mainResponse = mainClient.exchange('/management-port', String) - def healthResponse = mainClient.exchange('/health', String) + def healthResponse = managementClient.exchange('/health', String) then: mainResponse.body() == 'Hello world' healthResponse.body() == '{"status":"UP"}' + when: + mainClient.exchange('/health', String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.NOT_FOUND + cleanup: mainClient.close() managementClient.close() @@ -112,7 +126,7 @@ class TomcatManagementPortSpec extends Specification { } @Controller("/management-port") - @Requires(property = "spec.name", value = "JettyManagementPortSpec") + @Requires(property = "spec.name", value = "TomcatManagementPortSpec") static class TestController { @Get diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index 366d1ff00..f57845444 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -19,14 +19,17 @@ import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Primary; import io.micronaut.context.env.Environment; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.ResourceResolver; import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.server.exceptions.ServerStartupException; import io.micronaut.http.ssl.SslConfiguration; import io.micronaut.servlet.engine.MicronautServletConfiguration; import io.micronaut.servlet.engine.initializer.MicronautServletInitializer; import io.micronaut.servlet.engine.server.ServletServerFactory; import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; +import io.micronaut.web.router.Router; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.UndertowOptions; @@ -57,6 +60,7 @@ public class UndertowFactory extends ServletServerFactory { private final UndertowConfiguration configuration; + private final Router router; /** * Default constructor. @@ -75,6 +79,7 @@ public UndertowFactory( List staticResourceConfigurations) { super(resourceResolver, configuration, sslConfiguration, applicationContext, staticResourceConfigurations); this.configuration = configuration; + this.router = applicationContext.findBean(Router.class).orElse(null); } /** @@ -125,11 +130,13 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron host ); } + applyAdditionalPorts(builder, host, port, sslContext); } else { builder.addHttpListener( port, host ); + applyAdditionalPorts(builder, host, port, null); } } else { @@ -137,6 +144,7 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron port, host ); + applyAdditionalPorts(builder, host, port, null); } Map serverOptions = configuration.getServerOptions(); @@ -183,6 +191,27 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron return builder; } + private void applyAdditionalPorts(Undertow.Builder builder, String host, int serverPort, @Nullable SSLContext sslContext) { + if (router != null) { + Set exposedPorts = router.getExposedPorts(); + if (CollectionUtils.isNotEmpty(exposedPorts)) { + for (Integer exposedPort : exposedPorts) { + if (!exposedPort.equals(serverPort)) { + addListener(builder, host, sslContext, exposedPort); + } + } + } + } + } + + private static void addListener(Undertow.Builder builder, String host, SSLContext sslContext, Integer exposedPort) { + if (sslContext != null) { + builder.addHttpsListener(exposedPort, host, sslContext); + } else { + builder.addHttpListener(exposedPort, host); + } + } + private Object getOptionValue(String key) { return ReflectionUtils.findDeclaredField(Options.class, key) .map(field -> { diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowServer.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowServer.java index e00636347..654cb7f0e 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowServer.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowServer.java @@ -23,6 +23,7 @@ import jakarta.inject.Singleton; import java.net.*; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @@ -54,10 +55,12 @@ public UndertowServer( protected void startServer() throws Exception { Undertow server = getServer(); server.start(); - this.listenersByProtocol = server.getListenerInfo().stream().collect(Collectors.toMap( - Undertow.ListenerInfo::getProtcol, - (listenerInfo -> listenerInfo) - )); + this.listenersByProtocol = new HashMap<>(); + for (Undertow.ListenerInfo listenerInfo : server.getListenerInfo()) { + if (!listenersByProtocol.containsKey(listenerInfo.getProtcol())) { + listenersByProtocol.put(listenerInfo.getProtcol(), listenerInfo); + } + } } @Override diff --git a/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowManagementPortSpec.groovy b/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowManagementPortSpec.groovy index 115e50e2f..dff74c2ee 100644 --- a/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowManagementPortSpec.groovy +++ b/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowManagementPortSpec.groovy @@ -4,10 +4,12 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.core.io.socket.SocketUtils import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.client.BlockingHttpClient import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.runtime.server.EmbeddedServer import io.netty.handler.ssl.util.SelfSignedCertificate import spock.lang.Issue @@ -59,12 +61,11 @@ class UndertowManagementPortSpec extends Specification { ] } - @PendingFeature def 'management port can be configured different to main port'() { given: def port = SocketUtils.findAvailableTcpPort() EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name' : 'JettyManagementPortSpec', + 'spec.name' : 'UndertowManagementPortSpec', 'endpoints.all.enabled': true, 'endpoints.all.port' : port, ]) @@ -73,24 +74,30 @@ class UndertowManagementPortSpec extends Specification { when: def mainResponse = mainClient.exchange('/management-port', String) - def healthResponse = mainClient.exchange('/health', String) + def healthResponse = managementClient.exchange('/health', String) then: mainResponse.body() == 'Hello world' healthResponse.body() == '{"status":"UP"}' + when: + mainClient.exchange('/health', String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.NOT_FOUND + cleanup: mainClient.close() managementClient.close() server.stop() } - @PendingFeature def 'management port can be configured different to main port and uses ssl if also configured'() { given: def port = SocketUtils.findAvailableTcpPort() EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ - 'spec.name' : 'JettyManagementPortSpec', + 'spec.name' : 'UndertowManagementPortSpec', 'endpoints.all.enabled': true, 'endpoints.all.port' : port, ] + sslConfig()) @@ -99,12 +106,19 @@ class UndertowManagementPortSpec extends Specification { when: def mainResponse = mainClient.exchange('/management-port', String) - def healthResponse = mainClient.exchange('/health', String) + def healthResponse = managementClient.exchange('/health', String) then: mainResponse.body() == 'Hello world' healthResponse.body() == '{"status":"UP"}' + when: + mainClient.exchange('/health', String) + + then: + def e = thrown(HttpClientResponseException) + e.response.status() == HttpStatus.NOT_FOUND + cleanup: mainClient.close() managementClient.close() @@ -112,7 +126,7 @@ class UndertowManagementPortSpec extends Specification { } @Controller("/management-port") - @Requires(property = "spec.name", value = "JettyManagementPortSpec") + @Requires(property = "spec.name", value = "UndertowManagementPortSpec") static class TestController { @Get diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java index 039e0b773..8909461b3 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java @@ -157,19 +157,34 @@ protected DefaultServletHttpRequest(ConversionService conversionService, public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { Objects.requireNonNull(conversionContext, "Conversion context cannot be null"); Objects.requireNonNull(name, NULL_KEY); - Object attribute = delegate.getAttribute(name.toString()); + Object attribute = null; + try { + attribute = delegate.getAttribute(name.toString()); + } catch (IllegalStateException e) { + // ignore, request not longer active + } return Optional.ofNullable(attribute) .flatMap(v -> conversionService.convert(v, conversionContext)); } @Override public Set names() { - return CollectionUtils.enumerationToSet(delegate.getAttributeNames()); + try { + return CollectionUtils.enumerationToSet(delegate.getAttributeNames()); + } catch (IllegalStateException e) { + // ignore, request no longer active + return Set.of(); + } } @Override public Collection values() { - return names().stream().map(delegate::getAttribute).toList(); + try { + return names().stream().map(delegate::getAttribute).toList(); + } catch (IllegalStateException e) { + // ignore, request no longer active + return Collections.emptyList(); + } } @Override diff --git a/src/main/docs/guide/knownIssues.adoc b/src/main/docs/guide/knownIssues.adoc index 974643368..e000696ec 100644 --- a/src/main/docs/guide/knownIssues.adoc +++ b/src/main/docs/guide/knownIssues.adoc @@ -9,14 +9,3 @@ It is not currently possible to use the HttpProxyClient with Servlet Filters. Local error handlers that require the request body to be reparsed will not work in Servlet based applications. The body is read from the request input-stream and so attempting to reparse it for the error handler will fail. -=== Management port - -With a Netty based server, you can https://docs.micronaut.io/latest/guide/#_management_port[configure a management port for the server]. -This is not currently supported with Servlet based servers, and management endpoints (when enabled) will be available on the same port as the main application. - -=== ServiceReadyEvent publication - -When using the Netty runtime, the `ServiceReadyEvent` is automatically published when the server is ready to accept requests. -This is not currently supported with Servlet based servers. - -If you wish to generate a `ServiceReadyEvent` you can do so manually by injecting a `ApplicationEventPublisher` bean and publishing the event yourself when your application is ready. From 435f54de16901c32ad53fc706f7fc2fe9cc084e0 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 28 May 2024 22:35:20 +0200 Subject: [PATCH 024/180] Support for access log for each servlet server implementation (#713) Fixes #238 --- .../servlet/jetty/JettyConfiguration.java | 72 ++++++++++ .../micronaut/servlet/jetty/JettyFactory.java | 10 ++ .../servlet/jetty/JettyAccessLogSpec.groovy | 37 ++++++ .../servlet/tomcat/TomcatConfiguration.java | 43 ++++++ .../servlet/tomcat/TomcatFactory.java | 14 ++ .../servlet/tomcat/TomcatAccessLogSpec.groovy | 34 +++++ .../undertow/UndertowConfiguration.java | 124 ++++++++++++++++-- .../servlet/undertow/UndertowFactory.java | 30 ++++- .../undertow/UndertowAccessLogSpec.groovy | 65 +++++++++ 9 files changed, 409 insertions(+), 20 deletions(-) create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyAccessLogSpec.groovy create mode 100644 http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatAccessLogSpec.groovy create mode 100644 http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowAccessLogSpec.groovy diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyConfiguration.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyConfiguration.java index 1f324ad20..4d04c5b5f 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyConfiguration.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyConfiguration.java @@ -18,12 +18,19 @@ import io.micronaut.context.annotation.ConfigurationBuilder; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.format.MapFormat; import io.micronaut.core.naming.conventions.StringConvention; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.Toggleable; import io.micronaut.http.server.HttpServerConfiguration; +import jakarta.inject.Inject; +import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.HttpConfiguration; import io.micronaut.core.annotation.Nullable; +import org.eclipse.jetty.server.RequestLogWriter; import org.eclipse.jetty.server.SecureRequestCustomizer; import java.util.Collections; @@ -42,6 +49,7 @@ public class JettyConfiguration extends HttpServerConfiguration { @ConfigurationBuilder protected HttpConfiguration httpConfiguration = new HttpConfiguration(); + private final JettyRequestLog requestLog; private final MultipartConfiguration multipartConfiguration; private Map initParameters; @@ -51,7 +59,17 @@ public class JettyConfiguration extends HttpServerConfiguration { * @param multipartConfiguration The multipart configuration. */ public JettyConfiguration(@Nullable MultipartConfiguration multipartConfiguration) { + this(null, null); + } + + /** + * Default constructor. + * @param multipartConfiguration The multipart configuration. + */ + @Inject + public JettyConfiguration(@Nullable MultipartConfiguration multipartConfiguration, @Nullable JettyRequestLog requestLog) { this.multipartConfiguration = multipartConfiguration; + this.requestLog = requestLog; } /** @@ -68,6 +86,13 @@ public Optional getMultipartConfiguration() { return Optional.ofNullable(multipartConfiguration); } + /** + * @return The request log configuration. + */ + public Optional getRequestLog() { + return Optional.ofNullable(requestLog); + } + /** * @return The servlet init parameters */ @@ -98,4 +123,51 @@ public void setInitParameters( public static class JettySslConfiguration extends SecureRequestCustomizer { } + /** + * Jetty access log configuration. + * + * @since 4.8.0 + */ + @ConfigurationProperties(JettyRequestLog.ACCESS_LOG) + @Requires(property = JettyRequestLog.ENABLED_PROPERTY, value = StringUtils.TRUE) + public static class JettyRequestLog implements Toggleable { + public static final String ACCESS_LOG = "access-log"; + public static final String ENABLED_PROPERTY = HttpServerConfiguration.PREFIX + ".jetty." + ACCESS_LOG + ".enabled"; + @ConfigurationBuilder(prefixes = "set", excludes = "eventListeners") + RequestLogWriter requestLogWriter = new RequestLogWriter(); + + private boolean enabled = true; + private String pattern = CustomRequestLog.EXTENDED_NCSA_FORMAT; + + @Override + public boolean isEnabled() { + return enabled; + } + + /** + * Whether access log is enabled. + * @param enabled True if it is enabled. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * The pattern to use for the access log. Defaults to {@code EXTENDED_NCSA_FORMAT}. + * + * @return The pattern. + */ + public @NonNull String getPattern() { + return pattern; + } + + /** + * Sets the pattern to use for the access log. Defaults to CustomRequestLog.EXTENDED_NCSA_FORMAT. + * + * @param pattern The pattern + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + } } diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index c110c3474..6211ab827 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -49,6 +49,7 @@ import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; @@ -147,6 +148,15 @@ protected Server jettyServer( Server server = newServer(applicationContext, configuration); + jettyConfiguration.getRequestLog().ifPresent(requestLog -> { + if (requestLog.isEnabled()) { + server.setRequestLog(new CustomRequestLog( + requestLog.requestLogWriter, + requestLog.getPattern() + )); + } + }); + final ServletContextHandler contextHandler = newJettyContext(server, contextPath); configureServletInitializer(server, contextHandler, servletContainerInitializers); diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyAccessLogSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyAccessLogSpec.groovy new file mode 100644 index 000000000..ab9686d3e --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyAccessLogSpec.groovy @@ -0,0 +1,37 @@ +package io.micronaut.servlet.jetty + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.test.support.TestPropertyProvider +import jakarta.inject.Inject +import org.eclipse.jetty.server.CustomRequestLog +import org.eclipse.jetty.server.Server +import spock.lang.Specification + +import java.nio.file.Files + +@MicronautTest +class JettyAccessLogSpec extends Specification implements TestPropertyProvider { + @Inject + JettyConfiguration.JettyRequestLog requestLog + + @Inject Server server + + void "test configuration"() { + expect: + requestLog.enabled + requestLog.requestLogWriter.retainDays == 10 + requestLog.requestLogWriter.fileName != null + requestLog.pattern == CustomRequestLog.NCSA_FORMAT + server.requestLog != null + } + + @Override + Map getProperties() { + return [ + "micronaut.server.jetty.access-log.enabled": true, + "micronaut.server.jetty.access-log.filename": Files.createTempFile('log', 'test').toAbsolutePath().toString(), + "micronaut.server.jetty.access-log.retain-days": 10, + "micronaut.server.jetty.access-log.pattern": CustomRequestLog.NCSA_FORMAT + ] + } +} diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatConfiguration.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatConfiguration.java index f0484bf98..bf20c099b 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatConfiguration.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatConfiguration.java @@ -15,6 +15,10 @@ */ package io.micronaut.servlet.tomcat; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.Toggleable; +import jakarta.inject.Inject; import java.util.Optional; import io.micronaut.context.annotation.ConfigurationBuilder; @@ -25,6 +29,7 @@ import io.micronaut.core.annotation.TypeHint; import io.micronaut.http.server.HttpServerConfiguration; import org.apache.catalina.connector.Connector; +import org.apache.catalina.valves.ExtendedAccessLogValve; import org.apache.coyote.ajp.AjpNio2Protocol; import org.apache.coyote.ajp.AjpNioProtocol; import org.apache.coyote.http11.Http11Nio2Protocol; @@ -54,6 +59,8 @@ public class TomcatConfiguration extends HttpServerConfiguration { private final MultipartConfiguration multipartConfiguration; private String protocol; + private AccessLogConfiguration accessLogConfiguration; + /** * Default constructor. * @param multipartConfiguration The multipart config @@ -97,4 +104,40 @@ public Optional getMultipartConfiguration() { return Optional.ofNullable(multipartConfiguration); } + /** + * @return The access log configuration. + * @since 4.8.0 + */ + public Optional getAccessLogConfiguration() { + return Optional.ofNullable(accessLogConfiguration); + } + + /** + * Sets the access log configuration. + * @param accessLogConfiguration The access log configuration. + * @since 4.8.0 + */ + @Inject + public void setAccessLogConfiguration(@Nullable AccessLogConfiguration accessLogConfiguration) { + this.accessLogConfiguration = accessLogConfiguration; + } + + /** + * The access log configuration. + * @since 4.8.0 + */ + @ConfigurationProperties(value = AccessLogConfiguration.PREFIX, excludes = {"next", "container"}) + @Requires(property = AccessLogConfiguration.ENABLED_PROPERTY, value = StringUtils.TRUE) + @SuppressWarnings("java:S110") + public static class AccessLogConfiguration extends ExtendedAccessLogValve implements Toggleable { + + public static final String PREFIX = "access-log"; + + public static final String ENABLED_PROPERTY = HttpServerConfiguration.PREFIX + ".tomcat." + PREFIX + ".enabled"; + + @Override + public boolean isEnabled() { + return super.enabled; + } + } } diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 7b4a2d479..89c77f31e 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -43,8 +43,10 @@ import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration; import jakarta.inject.Singleton; import java.util.Set; +import org.apache.catalina.Container; import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.ContainerBase; import org.apache.catalina.startup.Tomcat; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http2.Http2Protocol; @@ -127,6 +129,18 @@ protected Tomcat tomcatServer( configureServletInitializer(context, servletInitializers); configureConnectors(tomcat, connector, httpsConnector); + TomcatConfiguration serverConfiguration = getServerConfiguration(); + serverConfiguration.getAccessLogConfiguration().ifPresent(accessValve -> { + if (accessValve.isEnabled()) { + Container[] children = tomcat.getHost().findChildren(); + for (Container child : children) { + if (child instanceof ContainerBase containerBase) { + containerBase.addValve(accessValve); + } + } + } + }); + return tomcat; } diff --git a/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatAccessLogSpec.groovy b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatAccessLogSpec.groovy new file mode 100644 index 000000000..427abbaeb --- /dev/null +++ b/http-server-tomcat/src/test/groovy/io/micronaut/servlet/tomcat/TomcatAccessLogSpec.groovy @@ -0,0 +1,34 @@ +package io.micronaut.servlet.tomcat + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.test.support.TestPropertyProvider +import jakarta.inject.Inject +import org.apache.catalina.Valve +import org.apache.catalina.startup.Tomcat +import org.apache.catalina.valves.Constants +import spock.lang.Specification + +import java.nio.file.Files + +@MicronautTest +class TomcatAccessLogSpec extends Specification implements TestPropertyProvider { + @Inject TomcatConfiguration.AccessLogConfiguration accessLogConfiguration + @Inject Tomcat tomcat + + void "test access log"() { + expect: + accessLogConfiguration.enabled + accessLogConfiguration.pattern == Constants.AccessLog.COMBINED_PATTERN + def valves = tomcat.host.findChildren().first().pipeline.valves + valves + valves.first() instanceof TomcatConfiguration.AccessLogConfiguration + } + + @Override + Map getProperties() { + return [ + "micronaut.server.tomcat.access-log.enabled": true, + "micronaut.server.tomcat.access-log.pattern": Constants.AccessLog.COMBINED_PATTERN + ] + } +} diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowConfiguration.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowConfiguration.java index 3b0b49372..ed0f1dee3 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowConfiguration.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowConfiguration.java @@ -18,17 +18,24 @@ import io.micronaut.context.annotation.ConfigurationBuilder; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.TypeHint; import io.micronaut.core.convert.format.MapFormat; import io.micronaut.core.naming.conventions.StringConvention; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.Toggleable; import io.micronaut.http.server.HttpServerConfiguration; +import io.micronaut.scheduling.TaskExecutors; import io.undertow.Undertow; import io.undertow.UndertowOptions; - -import io.micronaut.core.annotation.Nullable; +import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; +import jakarta.inject.Inject; +import jakarta.inject.Named; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ExecutorService; /** * Configuration for the Undertow server. @@ -38,8 +45,8 @@ */ @ConfigurationProperties("undertow") @TypeHint( - value = {UndertowOptions.class, org.xnio.Option.class}, - accessType = TypeHint.AccessType.ALL_DECLARED_FIELDS + value = {UndertowOptions.class, org.xnio.Option.class}, + accessType = TypeHint.AccessType.ALL_DECLARED_FIELDS ) @Replaces(HttpServerConfiguration.class) public class UndertowConfiguration extends HttpServerConfiguration { @@ -48,18 +55,38 @@ public class UndertowConfiguration extends HttpServerConfiguration { protected Undertow.Builder undertowBuilder = Undertow.builder(); private final MultipartConfiguration multipartConfiguration; + private AccessLogConfiguration accessLogConfiguration; private Map workerOptions = new HashMap<>(5); private Map socketOptions = new HashMap<>(5); private Map serverOptions = new HashMap<>(5); /** * Default constructor. + * * @param multipartConfiguration The multipart configuration */ public UndertowConfiguration(@Nullable MultipartConfiguration multipartConfiguration) { this.multipartConfiguration = multipartConfiguration; } + /** + * @return The access log configuration. + */ + public Optional getAccessLogConfiguration() { + return Optional.ofNullable(accessLogConfiguration); + } + + /** + * The access log configuration. + * + * @param accessLogConfiguration Sets the access log configuration. + * @see AccessLogConfiguration + */ + @Inject + public void setAccessLogConfiguration(@Nullable AccessLogConfiguration accessLogConfiguration) { + this.accessLogConfiguration = accessLogConfiguration; + } + /** * @return The undertow builder */ @@ -83,12 +110,13 @@ public Map getWorkerOptions() { /** * Sets the worker options. + * * @param workerOptions The worker options */ public void setWorkerOptions( - @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED, - transformation = MapFormat.MapTransformation.FLAT) - Map workerOptions) { + @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED, + transformation = MapFormat.MapTransformation.FLAT) + Map workerOptions) { if (workerOptions != null) { this.workerOptions.putAll(workerOptions); } @@ -103,12 +131,13 @@ public Map getSocketOptions() { /** * Sets the socket options. + * * @param socketOptions The socket options */ public void setSocketOptions( - @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED, - transformation = MapFormat.MapTransformation.FLAT) - Map socketOptions) { + @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED, + transformation = MapFormat.MapTransformation.FLAT) + Map socketOptions) { if (socketOptions != null) { this.socketOptions.putAll(socketOptions); } @@ -125,11 +154,80 @@ public Map getServerOptions() { * @param serverOptions Sets the server options */ public void setServerOptions( - @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED, - transformation = MapFormat.MapTransformation.FLAT) - Map serverOptions) { + @MapFormat(keyFormat = StringConvention.UNDER_SCORE_SEPARATED, + transformation = MapFormat.MapTransformation.FLAT) + Map serverOptions) { if (serverOptions != null) { this.serverOptions.putAll(serverOptions); } } + + /** + * The access log configuration. + * + * @since 4.8.0 + */ + @ConfigurationProperties(AccessLogConfiguration.PREFIX) + @Requires(property = AccessLogConfiguration.ENABLED_PROPERTY, value = StringUtils.TRUE) + public static class AccessLogConfiguration implements Toggleable { + + public static final String PREFIX = "access-log"; + + public static final String ENABLED_PROPERTY = HttpServerConfiguration.PREFIX + ".undertow." + PREFIX + ".enabled"; + + @ConfigurationBuilder(prefixes = "set", excludes = {"logFileHeaderGenerator", "logWriteExecutor"}) + DefaultAccessLogReceiver.Builder builder = DefaultAccessLogReceiver.builder(); + + private boolean enabled = true; + private String pattern = "common"; + + /** + * Default constructor. + * @param executorService The executor to use + */ + public AccessLogConfiguration(@Named(TaskExecutors.BLOCKING) ExecutorService executorService) { + // default to blocking executor. + builder.setLogWriteExecutor(executorService); + builder.setLogBaseName("access-"); + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + /** + * @param enabled Sets whether enabled. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * @return The log pattern to use. + */ + public String getPattern() { + return pattern; + } + + /** + * Sets the underlow log pattern. + * + *

See https://undertow.io/javadoc/1.4.x/io/undertow/server/handlers/accesslog/AccessLogHandler.html for more.

+ * + * @param pattern The pattern + */ + public void setPattern(String pattern) { + if (StringUtils.isNotEmpty(pattern)) { + this.pattern = pattern; + } + } + + /** + * @return The log receiver builder. + */ + public DefaultAccessLogReceiver.Builder getBuilder() { + return builder; + } + } } diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index f57845444..0727ca403 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -33,7 +33,8 @@ import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.UndertowOptions; -import io.undertow.server.handlers.PathHandler; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.accesslog.AccessLogHandler; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; @@ -82,6 +83,11 @@ public UndertowFactory( this.router = applicationContext.findBean(Router.class).orElse(null); } + @Override + public UndertowConfiguration getServerConfiguration() { + return (UndertowConfiguration) super.getServerConfiguration(); + } + /** * The undertow builder bean. * @@ -101,14 +107,24 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron final DeploymentManager deploymentManager = Servlets.defaultContainer().addDeployment(deploymentInfo); deploymentManager .deploy(); - PathHandler path; + HttpHandler httpHandler; try { - path = Handlers.path(Handlers.redirect(cp)) + httpHandler = Handlers.path(Handlers.redirect(cp)) .addPrefixPath(cp, deploymentManager.start()); } catch (ServletException e) { throw new ServerStartupException("Error starting Undertow server: " + e.getMessage(), e); } - builder.setHandler(path); + UndertowConfiguration serverConfiguration = getServerConfiguration(); + UndertowConfiguration.AccessLogConfiguration accessLogConfiguration = serverConfiguration.getAccessLogConfiguration().orElse(null); + if (accessLogConfiguration != null) { + httpHandler = new AccessLogHandler( + httpHandler, + accessLogConfiguration.builder.build(), + accessLogConfiguration.getPattern(), + getApplicationContext().getClassLoader() + ); + } + builder.setHandler(httpHandler); final SslConfiguration sslConfiguration = getSslConfiguration(); if (sslConfiguration.isEnabled()) { @@ -261,9 +277,9 @@ protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletCon final String cp = getContextPath(); for (ServletContainerInitializer servletInitializer : servletInitializers) { if (servletInitializer instanceof MicronautServletInitializer micronautServletInitializer) { - getStaticResourceConfigurations().forEach(config -> { - micronautServletInitializer.addMicronautServletMapping(config.getMapping()); - }); + getStaticResourceConfigurations().forEach(config -> + micronautServletInitializer.addMicronautServletMapping(config.getMapping()) + ); } } DeploymentInfo deploymentInfo = Servlets.deployment() diff --git a/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowAccessLogSpec.groovy b/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowAccessLogSpec.groovy new file mode 100644 index 000000000..09368b4a8 --- /dev/null +++ b/http-server-undertow/src/test/groovy/io/micronaut/servlet/undertow/UndertowAccessLogSpec.groovy @@ -0,0 +1,65 @@ +package io.micronaut.servlet.undertow + +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Value +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.servlet.undertow.UndertowConfiguration.AccessLogConfiguration +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.test.support.TestPropertyProvider +import io.undertow.Undertow +import io.undertow.server.handlers.accesslog.AccessLogHandler +import jakarta.inject.Inject +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.nio.file.Files + +@MicronautTest +class UndertowAccessLogSpec extends Specification implements TestPropertyProvider { + @Inject AccessLogConfiguration accessLogConfiguration + @Inject Undertow undertow + @Inject + @Client("/") + HttpClient rxClient + + @Value("\${micronaut.server.undertow.access-log.output-directory}") + String log + + void 'test access log configuration'() { + given: + PollingConditions pollingConditions = new PollingConditions(timeout: 20) + expect: + accessLogConfiguration + accessLogConfiguration.pattern == 'combined' + undertow != null + undertow.listenerInfo[0].openListener.rootHandler instanceof AccessLogHandler + rxClient.toBlocking().retrieve("/log-me") == 'ok' + pollingConditions.eventually { + assert new File(log, "access-log").exists() + assert new File(log, "access-log").text + } + + } + + @Override + Map getProperties() { + return [ + 'spec.name':'UndertowAccessLogSpec', + "micronaut.server.undertow.access-log.enabled": true, + "micronaut.server.undertow.access-log.pattern": "combined", + "micronaut.server.undertow.access-log.output-directory": Files.createTempDirectory("test-log").toAbsolutePath().toString() + ] + } + + @Controller('/log-me') + @Requires(property = "spec.name", value = 'UndertowAccessLogSpec') + static class LogTestController { + @Get(produces = "text/plain") + String go() { + return "ok"; + } + } +} From 2c421073da915ec7ab29508cae5788a69e7ac223 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 29 May 2024 09:52:44 +0200 Subject: [PATCH 025/180] Document access log support and annotation support better (#715) --- .../servlet/jetty/JettyServletAnnotationSpec.groovy | 1 + .../io/micronaut/servlet/jetty/MyFilterFactory.java | 10 +++++++--- .../src/test/resources/application-test.yaml | 3 +++ src/main/docs/guide/jetty.adoc | 13 +++++++++++++ src/main/docs/guide/servletAnnotation.adoc | 12 ++++++++++++ src/main/docs/guide/tomcat.adoc | 11 +++++++++++ src/main/docs/guide/undertow.adoc | 11 +++++++++++ 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy index fef911e11..c3571cc62 100644 --- a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyServletAnnotationSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.servlet.jetty +import io.micronaut.context.annotation.Property import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client import io.micronaut.test.extensions.spock.annotation.MicronautTest diff --git a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java index b6454ab59..b8c335ee8 100644 --- a/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java +++ b/http-server-jetty/src/test/java/io/micronaut/servlet/jetty/MyFilterFactory.java @@ -1,5 +1,6 @@ package io.micronaut.servlet.jetty; +// tag::class[] import io.micronaut.context.annotation.Factory; import io.micronaut.core.annotation.Order; import io.micronaut.core.order.Ordered; @@ -12,11 +13,13 @@ import jakarta.servlet.ServletResponse; import java.io.IOException; -@Factory +@Factory // <1> public class MyFilterFactory { - @ServletFilterBean(filterName = "another", value = {"/extra-filter/*", "/extra-servlet/*"}) - @Order(Ordered.HIGHEST_PRECEDENCE) + @ServletFilterBean( + filterName = "another", // <2> + value = {"/extra-filter/*", "${my.filter.mapping}"}) // <3> + @Order(Ordered.HIGHEST_PRECEDENCE) // <4> Filter myOtherFilter() { return new GenericFilter() { @Override @@ -27,3 +30,4 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha }; } } +// end::class[] diff --git a/http-server-jetty/src/test/resources/application-test.yaml b/http-server-jetty/src/test/resources/application-test.yaml index 2cedad963..c8a4d7937 100644 --- a/http-server-jetty/src/test/resources/application-test.yaml +++ b/http-server-jetty/src/test/resources/application-test.yaml @@ -4,3 +4,6 @@ micronaut: security: enabled: false +my: + filter: + mapping: /extra-servlet/* diff --git a/src/main/docs/guide/jetty.adoc b/src/main/docs/guide/jetty.adoc index 9541c826d..b01c65297 100644 --- a/src/main/docs/guide/jetty.adoc +++ b/src/main/docs/guide/jetty.adoc @@ -21,3 +21,16 @@ include::http-server-jetty/src/test/java/io/micronaut/servlet/jetty/docs/JettySe include::http-server-jetty/src/test/java/io/micronaut/servlet/jetty/docs/JettyServerCustomizer.java[tags=class, indent=0] ---- +=== Access Log Configuration + +To configure the https://eclipse.dev/jetty/documentation/jetty-11/programming-guide/index.html#pg-server-http-request-logging[Jetty Access Log]: + +.Jetty Access Log Configuration +[configuration] +---- +micronaut.server.jetty.access-log.enabled: true +micronaut.server.jetty.access-log.filename: /tmp/access.log +micronaut.server.jetty.access-log.retain-days: 10 +micronaut.server.jetty.access-log.pattern: > + %{client}a - %u %t "%r" %s %O +---- diff --git a/src/main/docs/guide/servletAnnotation.adoc b/src/main/docs/guide/servletAnnotation.adoc index e9796a81a..dffe00f4a 100644 --- a/src/main/docs/guide/servletAnnotation.adoc +++ b/src/main/docs/guide/servletAnnotation.adoc @@ -16,3 +16,15 @@ In addition, you can use the following annotations on methods of https://docs.mi * ann:servlet.api.annotation.ServletBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webservlet[@WebServlet] but can be applied to a method of a factory to Register a new servlet. * ann:servlet.api.annotation.ServletFilterBean[] - Equivalent of https://jakarta.ee/specifications/servlet/5.0/apidocs/jakarta/servlet/annotation/webfilter[@WebFilter] but can be applied to a method of a factory to Register a new filter. + +The following example adds a new Servlet filter with the highest precedence: + +.Adding a Filter with a Factory +snippet::io.micronaut.servlet.jetty.MyFilterFactory[tags="class", indent=0, project="http-server-jetty"] + +<1> A `@Factory` bean is defined +<2> The ann:servlet.api.annotation.ServletFilterBean[] annotation is used and a filter name defined +<3> 1 or more mappings are defined. Note these can be resolved from property placeholder configuration if necessary. +<4> The order of the filter is defined. + +NOTE: Servlet Filters are not to be confused with https://docs.micronaut.io/latest/guide/#filters[Micronaut filters]. Servlet Filters always run before the Micronaut Servlet which in turn runs the Micronaut Filters hence it is not possible to place a Servlet Filter after Micronaut Filters. diff --git a/src/main/docs/guide/tomcat.adoc b/src/main/docs/guide/tomcat.adoc index 8807d1a7a..3be255098 100644 --- a/src/main/docs/guide/tomcat.adoc +++ b/src/main/docs/guide/tomcat.adoc @@ -21,3 +21,14 @@ include::http-server-tomcat/src/test/java/io/micronaut/servlet/tomcat/docs/Tomca include::http-server-tomcat/src/test/java/io/micronaut/servlet/tomcat/docs/TomcatServerCustomizer.java[tags=class, indent=0] ---- +=== Access Log Configuration + +To configure the https://tomcat.apache.org/tomcat-10.1-doc/config/valve.html#Access_Logging[Tomcat Access Log]: + +.Tomcat Access Log Configuration +[configuration] +---- +micronaut.server.tomcat.access-log.enabled: true, +micronaut.server.tomcat.access-log.pattern: combined +micronaut.server.tomcat.access-log.directory: /var/logs +---- diff --git a/src/main/docs/guide/undertow.adoc b/src/main/docs/guide/undertow.adoc index 4e2aaacbe..46a5ba461 100644 --- a/src/main/docs/guide/undertow.adoc +++ b/src/main/docs/guide/undertow.adoc @@ -21,3 +21,14 @@ include::http-server-undertow/src/test/java/io/micronaut/servlet/undertow/docs/U include::http-server-undertow/src/test/java/io/micronaut/servlet/undertow/docs/UndertowServerCustomizer.java[tags=class, indent=0] ---- +=== Access Log Configuration + +To configure the https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform/7.3/html/configuration_guide/configuring_the_web_server_undertow#access_logging[Undertow Access Log]: + +.Undertow Access Log Configuration +[configuration] +---- +micronaut.server.undertow.access-log.enabled: true, +micronaut.server.undertow.access-log.pattern: combined +micronaut.server.undertow.access-log.output-directory: /var/logs +---- From b709eef343a23f9af99644f792bd5068b55fe7bc Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 29 May 2024 08:02:28 +0000 Subject: [PATCH 026/180] [skip ci] Release v4.8.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index da165bc3b..b0acb17a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.8.0-SNAPSHOT +projectVersion=4.8.0 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 27bc58222b76c4e31f6f5842b2551c0388e451f3 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Wed, 29 May 2024 08:08:05 +0000 Subject: [PATCH 027/180] chore: Bump version to 4.8.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b0acb17a2..6c0a6accc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.8.0 +projectVersion=4.8.1-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From d1f272f1a24c62d39a5305e331b0fe5ae08124cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 12:14:24 +0200 Subject: [PATCH 028/180] fix(deps): update dependency io.micronaut.security:micronaut-security-bom to v4.8.0 (#716) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ed536203..60a263f35 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ bcpkix = "1.70" managed-jetty = '11.0.21' micronaut-reactor = "3.3.0" -micronaut-security = "4.7.0" +micronaut-security = "4.8.0" micronaut-serde = "2.9.0" micronaut-session = "4.3.0" micronaut-validation = "4.5.0" From d3e9a9a1ed2e5cfbc4621136216d51391eb60303 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 17:55:34 +0200 Subject: [PATCH 029/180] fix(deps): update dependency io.micronaut.serde:micronaut-serde-bom to v2.10.0 (#717) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60a263f35..521b2afa1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ managed-jetty = '11.0.21' micronaut-reactor = "3.3.0" micronaut-security = "4.8.0" -micronaut-serde = "2.9.0" +micronaut-serde = "2.10.0" micronaut-session = "4.3.0" micronaut-validation = "4.5.0" google-cloud-functions = '1.1.0' From 3b64762ceb15821fe8e873d6729dd550e3d8bf0b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 10:39:36 +0200 Subject: [PATCH 030/180] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.5.0 (#718) * fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.5.0 * Implement byteBody * address review --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: yawkat --- gradle/libs.versions.toml | 2 +- .../engine/DefaultServletHttpRequest.java | 132 ++++++++++++------ .../engine/body/AbstractServletByteBody.java | 32 +++++ .../engine/body/AvailableByteArrayBody.java | 92 ++++++++++++ 4 files changed, 217 insertions(+), 41 deletions(-) create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 521b2afa1..4847c02cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.4.10" +micronaut = "4.5.0" micronaut-docs = "2.0.0" micronaut-test = "4.0.1" diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java index 8909461b3..66b7e0fb7 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java @@ -22,14 +22,12 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.SupplierUtil; -import io.micronaut.http.FullHttpRequest; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpParameters; @@ -37,10 +35,15 @@ import io.micronaut.http.HttpVersion; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.ServerHttpRequest; +import io.micronaut.http.body.AvailableByteBody; +import io.micronaut.http.body.ByteBody; +import io.micronaut.http.body.CloseableAvailableByteBody; +import io.micronaut.http.body.CloseableByteBody; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.cookie.Cookies; +import io.micronaut.servlet.engine.body.AvailableByteArrayBody; import io.micronaut.servlet.http.BodyBuilder; -import io.micronaut.servlet.http.ByteArrayByteBuffer; import io.micronaut.servlet.http.ParsedBodyHolder; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpRequest; @@ -51,9 +54,17 @@ import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InterruptedIOException; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; @@ -68,13 +79,12 @@ import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import org.reactivestreams.Subscriber; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Sinks; /** * Implementation of {@link HttpRequest} ontop of the Servlet API. @@ -88,7 +98,7 @@ public final class DefaultServletHttpRequest implements ServletHttpRequest, ServletExchange, StreamedServletMessage, - FullHttpRequest, + ServerHttpRequest, ParsedBodyHolder { private static final Logger LOG = LoggerFactory.getLogger(DefaultServletHttpRequest.class); @@ -103,11 +113,11 @@ public final class DefaultServletHttpRequest implements private final DefaultServletHttpResponse response; private final MediaTypeCodecRegistry codecRegistry; private final MutableConvertibleValues attributes; + private final StreamingBodyImpl byteBody = new StreamingBodyImpl(); private DefaultServletCookies cookies; private Supplier> body; private boolean bodyIsReadAsync; - private ByteArrayByteBuffer servletByteBuffer; private B parsedBody; /** @@ -323,7 +333,18 @@ public String getContextPath() { @Override public InputStream getInputStream() throws IOException { - return servletByteBuffer != null ? servletByteBuffer.toInputStream() : delegate.getInputStream(); + CompletableFuture buffered = byteBody.buffered.get(); + if (buffered == null) { + return delegate.getInputStream(); + } else { + try (CloseableAvailableByteBody split = buffered.get().split()) { + return split.toInputStream(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } catch (ExecutionException e) { + throw new IOException(e); + } + } } @Override @@ -489,35 +510,8 @@ public void onError(Throwable t) { } @Override - public boolean isFull() { - return !bodyIsReadAsync; - } - - @Override - public ByteBuffer contents() { - if (bodyIsReadAsync) { - if (LOG.isDebugEnabled()) { - LOG.debug("Body is read asynchronously, cannot get contents"); - } - return null; - } - try { - if (servletByteBuffer == null) { - this.servletByteBuffer = new ByteArrayByteBuffer<>(delegate.getInputStream().readAllBytes()); - } - return servletByteBuffer; - } catch (IOException e) { - throw new IllegalStateException("Error getting all body contents", e); - } - } - - @Override - public ExecutionFlow> bufferContents() { - ByteBuffer contents = contents(); - if (contents == null) { - return null; - } - return ExecutionFlow.just(contents); + public @NonNull ByteBody byteBody() { + return byteBody; } /** @@ -637,4 +631,62 @@ public Optional get(CharSequence name, ArgumentConversionContext conve return Optional.empty(); } } + + /** + * Temporary streaming {@link ByteBody} implementation that only supports buffering, for filter + * body binding to work. Will be replaced by a proper streaming implementation. + */ + private class StreamingBodyImpl implements CloseableByteBody { + private final AtomicReference> buffered = new AtomicReference<>(); + + @Override + public void close() { + } + + @Override + public @NonNull CloseableByteBody split(SplitBackpressureMode backpressureMode) { + return this; + } + + @Override + public @NonNull OptionalLong expectedLength() { + return OptionalLong.empty(); + } + + @Override + public @NonNull InputStream toInputStream() { + throw new UnsupportedOperationException("Streaming access not yet implemented for servlet"); + } + + @Override + public @NonNull Publisher toByteArrayPublisher() { + throw new UnsupportedOperationException("Streaming access not yet implemented for servlet"); + } + + @Override + public @NonNull Publisher> toByteBufferPublisher() { + throw new UnsupportedOperationException("Streaming access not yet implemented for servlet"); + } + + @Override + public CompletableFuture buffer() { + if (bodyIsReadAsync) { + throw new UnsupportedOperationException("Body is read asynchronously, cannot get contents"); + } + CompletableFuture dest = new CompletableFuture<>(); + CompletableFuture result; + if (buffered.compareAndSet(null, dest)) { + try (InputStream is = delegate.getInputStream()) { + dest.complete(new AvailableByteArrayBody(is.readAllBytes())); + } catch (Throwable t) { + dest.completeExceptionally(t); + } + result = dest; + } else { + result = buffered.get(); + } + // give each caller their own body + return result.thenApply(AvailableByteBody::split); + } + } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java new file mode 100644 index 000000000..bd3aba4d5 --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java @@ -0,0 +1,32 @@ +/* + * 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.servlet.engine.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.body.CloseableByteBody; + +/** + * Base class for servlet {@link io.micronaut.http.body.ByteBody} implementations. + * + * @author Jonas Konrad + * @since 4.9.0 + */ +@Internal +public abstract class AbstractServletByteBody implements CloseableByteBody { + static void failClaim() { + throw new IllegalStateException("Request body has already been claimed: Two conflicting sites are trying to access the request body. If this is intentional, the first user must ByteBody#split the body. To find out where the body was claimed, turn on TRACE logging for io.micronaut.http.server.netty.body.NettyByteBody."); + } +} diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java new file mode 100644 index 000000000..384aee930 --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java @@ -0,0 +1,92 @@ +/* + * 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.servlet.engine.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.http.body.CloseableAvailableByteBody; +import io.micronaut.servlet.http.ByteArrayByteBuffer; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; + +/** + * {@link io.micronaut.http.body.AvailableByteBody} implementation based on a byte array. + * + * @author Jonas Konrad + * @since 4.9.0 + */ +@Internal +public final class AvailableByteArrayBody extends AbstractServletByteBody implements CloseableAvailableByteBody { + private byte[] array; + + public AvailableByteArrayBody(byte[] array) { + this.array = array; + } + + @Override + public @NonNull CloseableAvailableByteBody split() { + if (array == null) { + failClaim(); + } + return new AvailableByteArrayBody(array); + } + + @Override + public @NonNull InputStream toInputStream() { + return new ByteArrayInputStream(array); + } + + @Override + public CompletableFuture buffer() { + if (array == null) { + failClaim(); + } + CompletableFuture f = CompletableFuture.completedFuture(new AvailableByteArrayBody(array)); + array = null; + return f; + } + + @Override + public long length() { + if (array == null) { + failClaim(); + } + return array.length; + } + + @Override + public byte @NonNull [] toByteArray() { + byte[] a = array; + if (a == null) { + failClaim(); + } + array = null; + return a; + } + + @Override + public @NonNull ByteBuffer toByteBuffer() { + return new ByteArrayByteBuffer<>(toByteArray()); + } + + @Override + public void close() { + array = null; + } +} From 829f1dbf9c4dd7022d9190ea5816560443e51707 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 31 May 2024 12:49:41 +0200 Subject: [PATCH 031/180] Update common files (#721) --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f8c8ad2db..78142de20 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -52,7 +52,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.3.1 + uses: gradle/gradle-build-action@v3.3.2 - name: "❓ Optional setup step" run: | From 749ed8ebce24ef5d15595a58699c44b7ee38e855 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Fri, 31 May 2024 19:51:38 +0200 Subject: [PATCH 032/180] Proper streaming ByteBody implementation (#720) * Proper streaming ByteBody implementation --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../servlet/http/ServletBodyBinder.java | 14 +- .../servlet/http/ServletHttpHandler.java | 111 +++--- .../http}/body/AbstractServletByteBody.java | 4 +- .../http}/body/AvailableByteArrayBody.java | 4 +- .../servlet/http/body/ByteQueue.java | 75 ++++ .../http/body/ExtendedInputStream.java | 142 +++++++ .../http/body/InputStreamByteBody.java | 151 ++++++++ .../servlet/http/body/StreamPair.java | 352 ++++++++++++++++++ .../servlet/http/body/StreamPairSpec.groovy | 149 ++++++++ .../engine/DefaultServletHttpHandler.java | 30 +- .../engine/DefaultServletHttpRequest.java | 94 +---- .../engine/LazyDelegateInputStream.java | 72 ++++ 12 files changed, 1049 insertions(+), 149 deletions(-) rename {servlet-engine/src/main/java/io/micronaut/servlet/engine => servlet-core/src/main/java/io/micronaut/servlet/http}/body/AbstractServletByteBody.java (91%) rename {servlet-engine/src/main/java/io/micronaut/servlet/engine => servlet-core/src/main/java/io/micronaut/servlet/http}/body/AvailableByteArrayBody.java (95%) create mode 100644 servlet-core/src/main/java/io/micronaut/servlet/http/body/ByteQueue.java create mode 100644 servlet-core/src/main/java/io/micronaut/servlet/http/body/ExtendedInputStream.java create mode 100644 servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java create mode 100644 servlet-core/src/main/java/io/micronaut/servlet/http/body/StreamPair.java create mode 100644 servlet-core/src/test/groovy/io/micronaut/servlet/http/body/StreamPairSpec.groovy create mode 100644 servlet-engine/src/main/java/io/micronaut/servlet/engine/LazyDelegateInputStream.java diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java index 3459832ea..3f2fc139f 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletBodyBinder.java @@ -123,7 +123,19 @@ public List getConversionErrors() { .orElse(null); if (name == null && messageBodyReader != null && messageBodyReader.isReadable(context.getArgument(), mediaType)) { try (InputStream inputStream = servletHttpRequest.getInputStream()) { - Object content = messageBodyReader.read(context.getArgument(), mediaType, source.getHeaders(), inputStream); + Object content; + if (Publishers.isConvertibleToPublisher(context.getArgument().getType())) { + Argument firstArg = context.getArgument().getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT); + Publisher publisher; + if (Publishers.isSingle(context.getArgument().getType())) { + publisher = Publishers.just(messageBodyReader.read(firstArg, mediaType, source.getHeaders(), inputStream)); + } else { + publisher = Flux.fromIterable((Iterable) messageBodyReader.read(Argument.listOf(firstArg), mediaType, source.getHeaders(), inputStream)); + } + content = conversionService.convertRequired(publisher, type); + } else { + content = messageBodyReader.read(context.getArgument(), mediaType, source.getHeaders(), inputStream); + } if (content != null && servletHttpRequest instanceof ParsedBodyHolder parsedBody) { parsedBody.setParsedBody(content); } diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index 88cca1d83..93d4779f9 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -37,9 +37,10 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Produces; +import io.micronaut.http.body.DynamicMessageBodyWriter; +import io.micronaut.http.body.MessageBodyHandlerRegistry; import io.micronaut.http.body.MessageBodyWriter; import io.micronaut.http.codec.CodecException; -import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.context.ServerHttpRequestContext; import io.micronaut.http.context.event.HttpRequestReceivedEvent; @@ -52,14 +53,13 @@ import io.micronaut.http.server.types.files.SystemFile; import io.micronaut.web.router.RouteInfo; import io.micronaut.web.router.resource.StaticResourceResolver; -import java.io.EOFException; 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.BufferedWriter; +import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -94,6 +94,7 @@ public abstract class ServletHttpHandler implements AutoCloseable, Lif private final RouteExecutor routeExecutor; private final ConversionService conversionService; private final MediaTypeCodecRegistry mediaTypeCodecRegistry; + private final MessageBodyHandlerRegistry messageBodyHandlerRegistry; private final Map, ServletResponseEncoder> responseEncoders; private final StaticResourceResolver staticResourceResolver; @@ -106,6 +107,7 @@ public abstract class ServletHttpHandler implements AutoCloseable, Lif protected ServletHttpHandler(ApplicationContext applicationContext, ConversionService conversionService) { this.applicationContext = Objects.requireNonNull(applicationContext, "The application context cannot be null"); this.mediaTypeCodecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); + this.messageBodyHandlerRegistry = applicationContext.getBean(MessageBodyHandlerRegistry.class); //noinspection unchecked this.responseEncoders = applicationContext.streamOfType(ServletResponseEncoder.class) .collect(Collectors.toMap( @@ -403,6 +405,14 @@ private void encodeResponse(ServletExchange exchange, response.contentType(mediaType); } + MessageBodyWriter messageBodyWriter = null; + if (!(body instanceof HttpStatus)) { + messageBodyWriter = routeInfoAttribute.map(RouteInfo::getMessageBodyWriter).orElse(null); + if (messageBodyWriter == null) { + messageBodyWriter = new DynamicMessageBodyWriter(messageBodyHandlerRegistry, List.of(mediaType)); + } + } + setHeadersFromMetadata(servletResponse, routeAnnotationMetadata, body); if (Publishers.isConvertibleToPublisher(body)) { boolean isSingle = Publishers.isSingle(body.getClass()); @@ -438,69 +448,54 @@ private void encodeResponse(ServletExchange exchange, return; } else { // fallback to blocking - body = Flux.from(publisher).collectList().block(); - servletResponse.body(body); + try (OutputStream outputStream = servletResponse.getOutputStream()) { + boolean json = mediaType.equals(MediaType.APPLICATION_JSON_TYPE); + if (json) { + outputStream.write('['); + } + boolean first = true; + for (Object o : Flux.from(publisher).toIterable()) { + if (!first && json) { + outputStream.write(','); + } + first = false; + + messageBodyWriter.writeTo( + bodyArgument, + mediaType, + o, + response.getHeaders(), + outputStream + ); + } + if (json) { + outputStream.write(']'); + } + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + responsePublisherCallback.accept(response); + return; } } } if (body instanceof HttpStatus httpStatus) { servletResponse.status(httpStatus); } else { - if (body instanceof CharSequence) { - if (response.getContentType().isEmpty()) { - response.contentType(MediaType.APPLICATION_JSON); - } - try (BufferedWriter writer = servletResponse.getWriter()) { - writer.write(body.toString()); - writer.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else if (body instanceof byte[] byteArray) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - outputStream.write(byteArray); - outputStream.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else if (body instanceof Writable writable) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - writable.writeTo(outputStream, response.getCharacterEncoding()); - outputStream.flush(); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else { - MessageBodyWriter messageBodyWriter = - routeInfoAttribute.map(RouteInfo::getMessageBodyWriter).orElse(null); - if (messageBodyWriter != null && messageBodyWriter.isWriteable(bodyArgument, mediaType)) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - messageBodyWriter.writeTo( - bodyArgument, - mediaType, - body, - response.getHeaders(), - outputStream - ); - } catch (IOException e) { - throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } + try (OutputStream outputStream = servletResponse.getOutputStream()) { + if (body instanceof Writable w) { + w.writeTo(outputStream); } else { - final MediaTypeCodec codec = mediaTypeCodecRegistry.findCodec(mediaType, bodyType).orElse(null); - if (codec != null) { - try (OutputStream outputStream = servletResponse.getOutputStream()) { - codec.encode(body, outputStream); - outputStream.flush(); - } catch (Throwable e) { - if (e instanceof CodecException codecException) { - throw codecException; - } - throw new CodecException("Failed to encode object [" + body + "] to content type [" + mediaType + "]: " + e.getMessage(), e); - } - } else { - throw new CodecException("No codec present capable of encoding object [" + body + "] to content type [" + mediaType + "]"); - } + messageBodyWriter.writeTo( + bodyArgument, + mediaType, + body, + response.getHeaders(), + outputStream + ); } + } catch (IOException e) { + throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AbstractServletByteBody.java similarity index 91% rename from servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java rename to servlet-core/src/main/java/io/micronaut/servlet/http/body/AbstractServletByteBody.java index bd3aba4d5..8f975bd51 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AbstractServletByteBody.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AbstractServletByteBody.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.servlet.engine.body; +package io.micronaut.servlet.http.body; import io.micronaut.core.annotation.Internal; import io.micronaut.http.body.CloseableByteBody; @@ -25,7 +25,7 @@ * @since 4.9.0 */ @Internal -public abstract class AbstractServletByteBody implements CloseableByteBody { +abstract class AbstractServletByteBody implements CloseableByteBody { static void failClaim() { throw new IllegalStateException("Request body has already been claimed: Two conflicting sites are trying to access the request body. If this is intentional, the first user must ByteBody#split the body. To find out where the body was claimed, turn on TRACE logging for io.micronaut.http.server.netty.body.NettyByteBody."); } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java similarity index 95% rename from servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java rename to servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java index 384aee930..ba6e3a049 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/body/AvailableByteArrayBody.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.servlet.engine.body; +package io.micronaut.servlet.http.body; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -27,6 +27,8 @@ /** * {@link io.micronaut.http.body.AvailableByteBody} implementation based on a byte array. + *

+ * Note: While internal, this is also used from the AWS and GCP modules. * * @author Jonas Konrad * @since 4.9.0 diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/ByteQueue.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/ByteQueue.java new file mode 100644 index 000000000..dbe3fb3a0 --- /dev/null +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/ByteQueue.java @@ -0,0 +1,75 @@ +/* + * 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.servlet.http.body; + +import io.micronaut.core.annotation.Internal; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Non-thread-safe queue for bytes. + * + * @author Jonas Konrad + * @since 4.9.0 + */ +@Internal +final class ByteQueue { + // not the most efficient implementation, but the most readable. + + private final Queue queue = new ArrayDeque<>(); + + /** + * Add a copy of the given array to this queue. + * + * @param arr The input array + * @param off The offset of the section to add + * @param len The length of the section to add + */ + public void addCopy(byte[] arr, int off, int len) { + add(Arrays.copyOfRange(arr, off, off + len)); + } + + private void add(byte[] arr) { + if (arr.length == 0) { + return; + } + queue.add(ByteBuffer.wrap(arr)); + } + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public int take(byte[] arr, int off, int len) { + ByteBuffer peek = queue.peek(); + if (peek == null) { + throw new IllegalStateException("Queue is empty"); + } + int n = Math.min(len, peek.remaining()); + peek.get(arr, off, n); + if (peek.remaining() == 0) { + queue.poll(); + } + return n; + } + + public void clear() { + queue.clear(); + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/ExtendedInputStream.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/ExtendedInputStream.java new file mode 100644 index 000000000..513af47a5 --- /dev/null +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/ExtendedInputStream.java @@ -0,0 +1,142 @@ +/* + * 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.servlet.http.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.body.ByteBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Extended InputStream API for better backpressure/cancellation handling. + * + * @author Jonas Konrad + * @since 4.9.0 + */ +@Internal +abstract class ExtendedInputStream extends InputStream { + private static final int CHUNK_SIZE = 8192; + private static final Logger LOG = LoggerFactory.getLogger(ExtendedInputStream.class); + + static ExtendedInputStream wrap(InputStream inputStream) { + return new Wrapper(inputStream); + } + + @Override + public int read() throws IOException { + byte[] arr1 = new byte[1]; + int n = read(arr1); + if (n == -1) { + return -1; + } else if (n == 0) { + throw new IllegalStateException("Read 0 bytes"); + } else { + return arr1[0] & 0xff; + } + } + + @Override + public abstract int read(byte[] b, int off, int len) throws IOException; + + /** + * Read some data into a new byte array. The array may be of any size. This is usually the same + * as allocating a new array, calling {@link #read(byte[])}, and then truncating the array, but + * may be optimized in some implementations. + */ + @Nullable + public byte[] readSome() throws IOException { + byte[] arr = new byte[CHUNK_SIZE]; + int n = read(arr); + if (n == -1) { + return null; + } else if (n == arr.length) { + return arr; + } else { + return Arrays.copyOf(arr, n); + } + } + + @Override + public void close() { + allowDiscard(); + cancelInput(); + } + + /** + * Allow discarding the input of this stream. See {@link ByteBody#allowDiscard()}. + */ + public abstract void allowDiscard(); + + /** + * Cancel any further upstream input. This also removes any backpressure that this stream + * may apply on its upstream. + */ + public abstract void cancelInput(); + + private static final class Wrapper extends ExtendedInputStream { + private final Lock lock = new ReentrantLock(); + private final InputStream delegate; + private boolean discarded; + + Wrapper(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + lock.lock(); + try { + if (discarded) { + throw ByteBody.BodyDiscardedException.create(); + } + return delegate.read(b, off, len); + } finally { + lock.unlock(); + } + } + + @Override + public void close() { + try { + delegate.close(); + } catch (IOException e) { + LOG.debug("Failed to close request stream", e); + } + } + + @Override + public void allowDiscard() { + lock.lock(); + try { + discarded = true; + close(); + } finally { + lock.unlock(); + } + } + + @Override + public void cancelInput() { + } + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java new file mode 100644 index 000000000..1417133b2 --- /dev/null +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java @@ -0,0 +1,151 @@ +/* + * 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.servlet.http.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.http.body.CloseableAvailableByteBody; +import io.micronaut.http.body.CloseableByteBody; +import io.micronaut.servlet.http.ByteArrayByteBuffer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; +import reactor.core.scheduler.Schedulers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Streaming {@link io.micronaut.http.body.ByteBody} implementation for servlet. + * + * @since 4.9.0 + * @author Jonas Konrad + */ +@Internal +public final class InputStreamByteBody extends AbstractServletByteBody { + private final Context context; + private ExtendedInputStream stream; + + private InputStreamByteBody(Context context, ExtendedInputStream stream) { + this.context = context; + this.stream = stream; + } + + public static InputStreamByteBody create(InputStream stream, OptionalLong length, Executor ioExecutor) { + return create(ExtendedInputStream.wrap(stream), length, ioExecutor); + } + + static InputStreamByteBody create(ExtendedInputStream stream, OptionalLong length, Executor ioExecutor) { + return new InputStreamByteBody(new Context(length, ioExecutor), stream); + } + + @Override + public @NonNull CloseableByteBody allowDiscard() { + stream.allowDiscard(); + return this; + } + + @Override + public void close() { + if (stream != null) { + stream.close(); + stream = null; + } + } + + @Override + public @NonNull CloseableByteBody split(SplitBackpressureMode backpressureMode) { + if (stream == null) { + failClaim(); + } + StreamPair.Pair pair = StreamPair.createStreamPair(stream, backpressureMode); + stream = pair.left(); + return new InputStreamByteBody(context, pair.right()); + } + + @Override + public @NonNull OptionalLong expectedLength() { + return context.expectedLength(); + } + + @Override + public @NonNull ExtendedInputStream toInputStream() { + ExtendedInputStream s = stream; + if (s == null) { + failClaim(); + } + stream = null; + return s; + } + + @Override + public @NonNull Flux toByteArrayPublisher() { + ExtendedInputStream s = toInputStream(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + return sink.asFlux() + .doOnRequest(req -> { + long remaining = req; + while (remaining > 0) { + @Nullable byte[] arr; + try { + arr = s.readSome(); + } catch (IOException e) { + sink.tryEmitError(e); + break; + } + if (arr == null) { + sink.tryEmitComplete(); + break; + } else { + remaining--; + sink.tryEmitNext(arr); + } + } + }) + .doOnTerminate(s::close) + .doOnCancel(s::close) + .subscribeOn(Schedulers.fromExecutor(context.ioExecutor())); + } + + @Override + public @NonNull Publisher> toByteBufferPublisher() { + return toByteArrayPublisher().map(ByteArrayByteBuffer::new); + } + + @Override + public CompletableFuture buffer() { + ExtendedInputStream s = toInputStream(); + return CompletableFuture.supplyAsync(() -> { + try (ExtendedInputStream t = s) { + return new AvailableByteArrayBody(t.readAllBytes()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, context.ioExecutor); + } + + private record Context( + OptionalLong expectedLength, + Executor ioExecutor + ) { + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/StreamPair.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/StreamPair.java new file mode 100644 index 000000000..f2fec9a2c --- /dev/null +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/StreamPair.java @@ -0,0 +1,352 @@ +/* + * 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.servlet.http.body; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.body.ByteBody; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * This class splits a single stream into two, based on configured + * {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode}. + * + * @since 4.9.0 + * @author Jonas Konrad + */ +@Internal +final class StreamPair { + private static final int FLAG_DISCARD_L = 1; + private static final int FLAG_DISCARD_R = 1 << 1; + private static final int MASK_DISCARD = FLAG_DISCARD_L | FLAG_DISCARD_R; + private static final int FLAG_CANCEL_L = 1 << 2; + private static final int FLAG_CANCEL_R = 1 << 3; + private static final int MASK_CANCEL = FLAG_CANCEL_L | FLAG_CANCEL_R; + + private final Lock lock = new ReentrantLock(); + private final Condition wakeup = lock.newCondition(); + private final AtomicInteger flags = new AtomicInteger(); + private final ExtendedInputStream upstream; + + /** + * For SLOWEST mode, if one side is currently waiting for the other, this contains the demand + * information. + */ + private Slowest.SlowestDemand slowestDemand = null; + + /** + * For all modes except SLOWEST, the queue of unprocessed bytes for the slower side. + */ + private ByteQueue queue; + /** + * For FASTEST mode, this is the {@link Side#left} flag of the side that is currently slower, + * i.e. that will read from {@link #queue}. + */ + private boolean fastModeSlowerSide; + /** + * For ORIGINAL and NEW modes, this flag is set to {@code true} when the upstream is finished. + */ + private boolean singleSideComplete; + /** + * For ORIGINAL and NEW modes, any read exception. + */ + private IOException singleSideException; + + private StreamPair(ExtendedInputStream upstream) { + this.upstream = upstream; + } + + private int getAndSetFlag(int flag) { + return flags.getAndUpdate(f -> f | flag); + } + + private boolean setFlagAndCheckMask(int flag, int mask) { + int old = getAndSetFlag(flag); + return (old & mask) != mask && ((old | flag) & mask) == mask; + } + + static Pair createStreamPair(ExtendedInputStream upstream, ByteBody.SplitBackpressureMode backpressureMode) { + StreamPair pair = new StreamPair(upstream); + return switch (backpressureMode) { + case SLOWEST -> new Pair(pair.new Slowest(true), pair.new Slowest(false)); + case FASTEST -> { + pair.queue = new ByteQueue(); + yield new Pair(pair.new Fastest(true), pair.new Fastest(false)); + } + case ORIGINAL -> { + pair.queue = new ByteQueue(); + yield new Pair(pair.new Preferred(), pair.new Listening()); + } + case NEW -> { + pair.queue = new ByteQueue(); + yield new Pair(pair.new Listening(), pair.new Preferred()); + } + }; + } + + record Pair(ExtendedInputStream left, ExtendedInputStream right) { + } + + private abstract class Side extends ExtendedInputStream { + final boolean left; + + private Side(boolean left) { + this.left = left; + } + + @Override + public void allowDiscard() { + if (setFlagAndCheckMask(left ? FLAG_DISCARD_L : FLAG_DISCARD_R, MASK_DISCARD)) { + upstream.allowDiscard(); + } + } + + @Override + public void cancelInput() { + if (setFlagAndCheckMask(left ? FLAG_CANCEL_L : FLAG_CANCEL_R, MASK_CANCEL)) { + upstream.cancelInput(); + } + } + + final boolean isOtherSideCancelled() { + return (flags.get() & (left ? FLAG_CANCEL_R : FLAG_CANCEL_L)) != 0; + } + } + + /** + * Both sides of {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode#SLOWEST}. + */ + private final class Slowest extends Side { + private Slowest(boolean left) { + super(left); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + lock.lock(); + lockBody: try { + SlowestDemand theirDemand = slowestDemand; + if (theirDemand == null) { + // other side is not reading yet. wait for them. + SlowestDemand ourDemand = new SlowestDemand(b, off, len); + slowestDemand = ourDemand; + do { + if (isOtherSideCancelled()) { + slowestDemand = null; + // other side should be disregarded. we must exit the lock here to + // avoid long blocking of any further disregardBackpressure calls on + // the other side. + break lockBody; + } + + try { + wakeup.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (!ourDemand.fulfilled) { + // not fulfilled + slowestDemand = null; + throw new InterruptedIOException(); + } + } + } while (!ourDemand.fulfilled); // guard for spurious wakeup + if (ourDemand.exception != null) { + throw ourDemand.exception; + } + return ourDemand.actualLength; + } else { + // other side is waiting for us. read some data and send it to them. + int n = Math.min(len, theirDemand.len); + try { + int actualLength = upstream.read(b, off, n); + if (actualLength >= 0) { + System.arraycopy(b, off, theirDemand.dest, theirDemand.off, actualLength); + } + theirDemand.actualLength = actualLength; + theirDemand.fulfilled = true; + slowestDemand = null; + wakeup.signalAll(); + return actualLength; + } catch (IOException e) { + theirDemand.exception = e; + theirDemand.fulfilled = true; + slowestDemand = null; + wakeup.signalAll(); + throw e; + } + } + } finally { + lock.unlock(); + } + // this is hit when the other side has cancelled their input, see above. + return upstream.read(b, off, len); + } + + @Override + public void cancelInput() { + super.cancelInput(); + // if the other side is waiting on us, wake it up. + lock.lock(); + try { + wakeup.signalAll(); + } finally { + lock.unlock(); + } + } + + static class SlowestDemand { + final byte[] dest; + final int off; + final int len; + boolean fulfilled; + IOException exception; + int actualLength; + + SlowestDemand(byte[] dest, int off, int len) { + this.dest = dest; + this.off = off; + this.len = len; + } + } + } + + /** + * Both sides of {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode#FASTEST}. + */ + private final class Fastest extends Side { + private Fastest(boolean left) { + super(left); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + lock.lock(); + try { + if (!queue.isEmpty() && fastModeSlowerSide == left) { + return queue.take(b, off, len); + } else { + int n = upstream.read(b, off, len); + if (n == -1) { + return -1; + } + if (!isOtherSideCancelled()) { + fastModeSlowerSide = !left; + queue.addCopy(b, off, n); + } else { + // discard queue here because we already hold the lock + queue.clear(); + } + return n; + } + } finally { + lock.unlock(); + } + } + } + + /** + * Original side of {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode#ORIGINAL}, or + * new side of {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode#NEW}. + */ + private final class Preferred extends Side { + Preferred() { + super(true); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + lock.lock(); + try { + int n = upstream.read(b, off, len); + if (n == -1) { + singleSideComplete = true; + } else if (!isOtherSideCancelled()) { + queue.addCopy(b, off, n); + } else { + // discard queue here because we already hold the lock + queue.clear(); + } + // in case other side is waiting, wake them + wakeup.signalAll(); + return n; + } catch (IOException e) { + singleSideException = e; + wakeup.signalAll(); + throw e; + } finally { + lock.unlock(); + } + } + + @Override + public void cancelInput() { + super.cancelInput(); + lock.lock(); + try { + // signal other side that it's time to take over + wakeup.signalAll(); + } finally { + lock.unlock(); + } + } + } + + /** + * New side of {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode#ORIGINAL}, or + * original side of {@link io.micronaut.http.body.ByteBody.SplitBackpressureMode#NEW}. + */ + private final class Listening extends Side { + Listening() { + super(false); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + lock.lock(); + try { + while (true) { + if (!queue.isEmpty()) { + return queue.take(b, off, len); + } + if (singleSideException != null) { + throw singleSideException; + } + if (singleSideComplete) { + return -1; + } + if (isOtherSideCancelled()) { + // exit lock and take over reading + break; + } + // wait for other side to read some data and wake us + wakeup.await(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(); + } finally { + lock.unlock(); + } + // only hit if other side has cancelled. in that case, directly read + return upstream.read(b, off, len); + } + } +} diff --git a/servlet-core/src/test/groovy/io/micronaut/servlet/http/body/StreamPairSpec.groovy b/servlet-core/src/test/groovy/io/micronaut/servlet/http/body/StreamPairSpec.groovy new file mode 100644 index 000000000..c238fe340 --- /dev/null +++ b/servlet-core/src/test/groovy/io/micronaut/servlet/http/body/StreamPairSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.servlet.http.body + +import io.micronaut.http.body.ByteBody +import spock.lang.Specification +import spock.lang.Timeout + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ThreadLocalRandom +import java.util.concurrent.TimeUnit + +@Timeout(10) +class StreamPairSpec extends Specification { + private ExecutorService executor + + static byte[] bytes(int n) { + def data = new byte[n] + ThreadLocalRandom.current().nextBytes(data) + return data + } + + def setup() { + executor = Executors.newCachedThreadPool() + } + + def cleanup() { + executor.shutdown() + } + + def slowest() { + given: + def data = bytes(100) + def p = StreamPair.createStreamPair(ExtendedInputStream.wrap(new ByteArrayInputStream(data)), ByteBody.SplitBackpressureMode.SLOWEST) + def f1 = executor.submit { + assert Arrays.equals(p.left().readAllBytes(), data) + } + TimeUnit.MILLISECONDS.sleep(10) + expect: + !f1.isDone() + + when: + def f2 = executor.submit { + assert Arrays.equals(p.right().readAllBytes(), data) + } + f1.get() + f2.get() + then: + noExceptionThrown() + } + + def fastest() { + given: + def data = bytes(100) + def p = StreamPair.createStreamPair(ExtendedInputStream.wrap(new ByteArrayInputStream(data)), ByteBody.SplitBackpressureMode.FASTEST) + assert Arrays.equals(p.left().readAllBytes(), data) + assert Arrays.equals(p.right().readAllBytes(), data) + } + + def original() { + given: + def data = bytes(100) + def p = StreamPair.createStreamPair(ExtendedInputStream.wrap(new ByteArrayInputStream(data)), ByteBody.SplitBackpressureMode.ORIGINAL) + assert Arrays.equals(p.left().readNBytes(50), Arrays.copyOf(data, 50)) + assert Arrays.equals(p.right().readNBytes(30), Arrays.copyOf(data, 30)) + def f1 = executor.submit { + assert Arrays.equals(p.right().readAllBytes(), Arrays.copyOfRange(data, 30, 100)) + } + TimeUnit.MILLISECONDS.sleep(10) + expect: + !f1.isDone() + + when: + assert Arrays.equals(p.left().readNBytes(50), Arrays.copyOfRange(data, 50, 100)) + f1.get() + then: + noExceptionThrown() + } + + def 'slowest cancellation'() { + given: + def data = bytes(100) + def p = StreamPair.createStreamPair(ExtendedInputStream.wrap(new ByteArrayInputStream(data)), ByteBody.SplitBackpressureMode.SLOWEST) + def f1 = executor.submit { + assert Arrays.equals(p.left().readAllBytes(), data) + } + TimeUnit.MILLISECONDS.sleep(10) + expect: + !f1.isDone() + + when: + p.right().cancelInput() + f1.get() + then: + noExceptionThrown() + } + + def 'original cancellation'() { + given: + def data = bytes(100) + def p = StreamPair.createStreamPair(ExtendedInputStream.wrap(new ByteArrayInputStream(data)), ByteBody.SplitBackpressureMode.ORIGINAL) + def f1 = executor.submit { + assert Arrays.equals(p.right().readAllBytes(), data) + } + TimeUnit.MILLISECONDS.sleep(10) + expect: + !f1.isDone() + + when: + p.left().cancelInput() + f1.get() + then: + noExceptionThrown() + } + + private class Data { + final byte[] data; + + Data(int n) { + this.data = new byte[n] + ThreadLocalRandom.current().nextBytes(this.data) + } + + InputStream input() { + return new InputStream() { + int i = 0 + + @Override + int read() throws IOException { + if (i >= data.length) { + return -1 + } else { + return data[i++] & 0xff + } + } + } + } + + InputStream check(InputStream s) { + return new InputStream() { + int i = 0 + + @Override + int read() throws IOException { + + } + } + } + } +} diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpHandler.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpHandler.java index 3814aa061..3e341aedb 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpHandler.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpHandler.java @@ -17,14 +17,18 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.scheduling.TaskExecutors; import io.micronaut.servlet.http.BodyBuilder; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpHandler; - +import jakarta.inject.Named; import jakarta.inject.Singleton; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + /** * Default implementation of {@link ServletHttpHandler} for the Servlet API. * @@ -33,32 +37,48 @@ */ @Singleton public class DefaultServletHttpHandler extends ServletHttpHandler { + private final Executor ioExecutor; + /** * Default constructor. * * @param applicationContext The application context * @param conversionService The conversion service + * @param ioExecutor Executor to use for blocking IO operations */ - public DefaultServletHttpHandler(ApplicationContext applicationContext, ConversionService conversionService) { + public DefaultServletHttpHandler(ApplicationContext applicationContext, ConversionService conversionService, @Named(TaskExecutors.BLOCKING) Executor ioExecutor) { super(applicationContext, conversionService); + this.ioExecutor = ioExecutor; + } + + /** + * Default constructor. + * + * @param applicationContext The application context + * @param conversionService The conversion service + * @deprecated use {@link #DefaultServletHttpHandler(ApplicationContext, ConversionService, Executor)} + */ + @Deprecated + public DefaultServletHttpHandler(ApplicationContext applicationContext, ConversionService conversionService) { + this(applicationContext, conversionService, ForkJoinPool.commonPool()); } /** * Default constructor. * * @param applicationContext The application context - * @deprecated use {@link #DefaultServletHttpHandler(ApplicationContext, ConversionService)} + * @deprecated use {@link #DefaultServletHttpHandler(ApplicationContext, ConversionService, Executor)} */ @Deprecated public DefaultServletHttpHandler(ApplicationContext applicationContext) { - super(applicationContext, ConversionService.SHARED); + this(applicationContext, ConversionService.SHARED); } @Override protected ServletExchange createExchange( HttpServletRequest request, HttpServletResponse response) { - return new DefaultServletHttpRequest<>(applicationContext.getConversionService(), request, response, getMediaTypeCodecRegistry(), applicationContext.getBean(BodyBuilder.class)); + return new DefaultServletHttpRequest<>(applicationContext.getConversionService(), request, response, getMediaTypeCodecRegistry(), applicationContext.getBean(BodyBuilder.class), ioExecutor); } @Override diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java index 66b7e0fb7..3d6abd0d7 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java @@ -22,7 +22,6 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; @@ -36,25 +35,22 @@ import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.ServerHttpRequest; -import io.micronaut.http.body.AvailableByteBody; import io.micronaut.http.body.ByteBody; -import io.micronaut.http.body.CloseableAvailableByteBody; import io.micronaut.http.body.CloseableByteBody; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.cookie.Cookies; -import io.micronaut.servlet.engine.body.AvailableByteArrayBody; import io.micronaut.servlet.http.BodyBuilder; import io.micronaut.servlet.http.ParsedBodyHolder; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpRequest; import io.micronaut.servlet.http.ServletHttpResponse; import io.micronaut.servlet.http.StreamedServletMessage; +import io.micronaut.servlet.http.body.InputStreamByteBody; import jakarta.servlet.AsyncContext; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +60,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InterruptedIOException; +import java.io.InputStreamReader; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; @@ -81,9 +77,7 @@ import java.util.Optional; import java.util.OptionalLong; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.Executor; import java.util.function.Supplier; /** @@ -113,7 +107,7 @@ public final class DefaultServletHttpRequest implements private final DefaultServletHttpResponse response; private final MediaTypeCodecRegistry codecRegistry; private final MutableConvertibleValues attributes; - private final StreamingBodyImpl byteBody = new StreamingBodyImpl(); + private final CloseableByteBody byteBody; private DefaultServletCookies cookies; private Supplier> body; @@ -128,16 +122,20 @@ public final class DefaultServletHttpRequest implements * @param response The servlet response * @param codecRegistry The codec registry * @param bodyBuilder Body Builder + * @param ioExecutor Executor for blocking operations */ protected DefaultServletHttpRequest(ConversionService conversionService, HttpServletRequest delegate, HttpServletResponse response, MediaTypeCodecRegistry codecRegistry, - BodyBuilder bodyBuilder) { + BodyBuilder bodyBuilder, + Executor ioExecutor) { super(); this.conversionService = conversionService; this.delegate = delegate; this.codecRegistry = codecRegistry; + long contentLengthLong = delegate.getContentLengthLong(); + this.byteBody = InputStreamByteBody.create(new LazyDelegateInputStream(delegate), contentLengthLong < 0 ? OptionalLong.empty() : OptionalLong.of(contentLengthLong), ioExecutor); String requestURI = delegate.getRequestURI(); @@ -331,25 +329,15 @@ public String getContextPath() { return delegate.getContextPath(); } + @SuppressWarnings("resource") @Override public InputStream getInputStream() throws IOException { - CompletableFuture buffered = byteBody.buffered.get(); - if (buffered == null) { - return delegate.getInputStream(); - } else { - try (CloseableAvailableByteBody split = buffered.get().split()) { - return split.toInputStream(); - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } catch (ExecutionException e) { - throw new IOException(e); - } - } + return byteBody().split(ByteBody.SplitBackpressureMode.FASTEST).toInputStream(); } @Override public BufferedReader getReader() throws IOException { - return delegate.getReader(); + return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); } @Override @@ -631,62 +619,4 @@ public Optional get(CharSequence name, ArgumentConversionContext conve return Optional.empty(); } } - - /** - * Temporary streaming {@link ByteBody} implementation that only supports buffering, for filter - * body binding to work. Will be replaced by a proper streaming implementation. - */ - private class StreamingBodyImpl implements CloseableByteBody { - private final AtomicReference> buffered = new AtomicReference<>(); - - @Override - public void close() { - } - - @Override - public @NonNull CloseableByteBody split(SplitBackpressureMode backpressureMode) { - return this; - } - - @Override - public @NonNull OptionalLong expectedLength() { - return OptionalLong.empty(); - } - - @Override - public @NonNull InputStream toInputStream() { - throw new UnsupportedOperationException("Streaming access not yet implemented for servlet"); - } - - @Override - public @NonNull Publisher toByteArrayPublisher() { - throw new UnsupportedOperationException("Streaming access not yet implemented for servlet"); - } - - @Override - public @NonNull Publisher> toByteBufferPublisher() { - throw new UnsupportedOperationException("Streaming access not yet implemented for servlet"); - } - - @Override - public CompletableFuture buffer() { - if (bodyIsReadAsync) { - throw new UnsupportedOperationException("Body is read asynchronously, cannot get contents"); - } - CompletableFuture dest = new CompletableFuture<>(); - CompletableFuture result; - if (buffered.compareAndSet(null, dest)) { - try (InputStream is = delegate.getInputStream()) { - dest.complete(new AvailableByteArrayBody(is.readAllBytes())); - } catch (Throwable t) { - dest.completeExceptionally(t); - } - result = dest; - } else { - result = buffered.get(); - } - // give each caller their own body - return result.thenApply(AvailableByteBody::split); - } - } } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/LazyDelegateInputStream.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/LazyDelegateInputStream.java new file mode 100644 index 000000000..7be65d51e --- /dev/null +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/LazyDelegateInputStream.java @@ -0,0 +1,72 @@ +/* + * 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.servlet.engine; + +import io.micronaut.core.annotation.Internal; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Delegating {@link InputStream} that only calls {@link HttpServletRequest#getInputStream()} on + * first read. + * + * @since 4.9.0 + * @author Jonas Konrad + */ +@Internal +final class LazyDelegateInputStream extends InputStream { + private HttpServletRequest request; + private InputStream delegate; + + LazyDelegateInputStream(HttpServletRequest request) { + this.request = request; + } + + private InputStream delegate() throws IOException { + if (delegate == null) { + delegate = request.getInputStream(); + request = null; + } + return delegate; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate().read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return delegate().skip(n); + } + + @Override + public int read() throws IOException { + return delegate().read(); + } + + @Override + public int available() throws IOException { + return delegate().available(); + } + + @Override + public void close() throws IOException { + delegate().close(); + } +} From 86de754bb2f5c6f6451215e4f714d2899337d8a5 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk <80816836+andriy-dmytruk@users.noreply.github.com> Date: Fri, 31 May 2024 13:52:00 -0400 Subject: [PATCH 033/180] Support configuring minThreads and maxThreads (#722) --- .../micronaut/servlet/jetty/JettyFactory.java | 18 ++++-- .../jetty/JettyConfigurationSpec.groovy | 58 +++++++++++++++++++ .../servlet/tomcat/TomcatFactory.java | 16 +++++ .../servlet/undertow/UndertowFactory.java | 6 ++ .../servlet/http/ServletConfiguration.java | 19 ++++++ .../engine/MicronautServletConfiguration.java | 31 ++++++++++ 6 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyConfigurationSpec.groovy diff --git a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java index 6211ab827..2e14e30e9 100644 --- a/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java +++ b/http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java @@ -365,17 +365,23 @@ private void applyAdditionalPorts(Server server, ServerConnector serverConnector * @return The server */ protected @NonNull Server newServer(@NonNull ApplicationContext applicationContext, @NonNull MicronautServletConfiguration configuration) { - Server server; + QueuedThreadPool threadPool; + if (configuration.getMaxThreads() != null) { + if (configuration.getMinThreads() != null) { + threadPool = new QueuedThreadPool(configuration.getMaxThreads(), configuration.getMinThreads()); + } else { + threadPool = new QueuedThreadPool(configuration.getMaxThreads()); + } + } else { + threadPool = new QueuedThreadPool(); + } + if (configuration.isEnableVirtualThreads() && LoomSupport.isSupported()) { - QueuedThreadPool threadPool = new QueuedThreadPool(); threadPool.setVirtualThreadsExecutor( applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)) ); - server = new Server(threadPool); - } else { - server = new Server(); } - return server; + return new Server(threadPool); } /** diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyConfigurationSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyConfigurationSpec.groovy new file mode 100644 index 000000000..e5e231511 --- /dev/null +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyConfigurationSpec.groovy @@ -0,0 +1,58 @@ +package io.micronaut.servlet.jetty + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.annotation.QueryValue +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.servlet.engine.MicronautServletConfiguration +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.Jetty +import spock.lang.Specification + +@MicronautTest +@Property(name = "spec.name", value = SPEC_NAME) +@Property(name = "micronaut.servlet.minThreads", value = "11") +@Property(name = "micronaut.servlet.maxThreads", value = "11") +class JettyConfigurationSpec extends Specification { + + static final String SPEC_NAME = "JettyConfigurationSpec" + + @Inject + Server jetty + + @Inject + @Client("/configTest") + HttpClient client + + void "configuring thread pool is supported"() { + when: + var threadPool = jetty.threadPool + + then: + threadPool.threads == 11 + + when: + def request = HttpRequest.GET("/") + String response = client.toBlocking().retrieve(request) + + then: + response == "OK" + } + + @Controller("/configTest") + static class TestController { + + @Get + @Produces(MediaType.TEXT_PLAIN) + String index() { + "OK" + } + } +} diff --git a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java index 89c77f31e..981917f50 100644 --- a/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java +++ b/http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java @@ -47,6 +47,7 @@ import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.ContainerBase; +import org.apache.catalina.core.StandardThreadExecutor; import org.apache.catalina.startup.Tomcat; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http2.Http2Protocol; @@ -124,6 +125,21 @@ protected Tomcat tomcatServer( configuration.setAsyncFileServingEnabled(false); Tomcat tomcat = newTomcat(); + if (configuration.getMaxThreads() != null) { + StandardThreadExecutor executor = new StandardThreadExecutor(); + executor.setName("tomcatThreadPool"); + executor.setMaxThreads(configuration.getMaxThreads()); + if (configuration.getMinThreads() != null) { + executor.setMinSpareThreads(configuration.getMinThreads()); + } + tomcat.getService().addExecutor(executor); + if (connector != null) { + connector.getProtocolHandler().setExecutor(executor); + } + if (httpsConnector != null) { + httpsConnector.getProtocolHandler().setExecutor(executor); + } + } final Context context = newTomcatContext(tomcat); configureServletInitializer(context, servletInitializers); diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index 0727ca403..3576ef784 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -163,6 +163,12 @@ protected Undertow.Builder undertowBuilder(DeploymentInfo deploymentInfo, Micron applyAdditionalPorts(builder, host, port, null); } + if (servletConfiguration.getMaxThreads() != null) { + builder.setServerOption(Options.WORKER_TASK_MAX_THREADS, servletConfiguration.getMaxThreads()); + if (servletConfiguration.getMinThreads() != null) { + builder.setServerOption(Options.WORKER_TASK_CORE_THREADS, servletConfiguration.getMinThreads()); + } + } Map serverOptions = configuration.getServerOptions(); serverOptions.forEach((key, value) -> { Object opt = ReflectionUtils.findDeclaredField(UndertowOptions.class, key) diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java index bd6bb1a74..c9a59471d 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java @@ -50,4 +50,23 @@ default boolean isAsyncSupported() { default boolean isEnableVirtualThreads() { return true; } + + /** + * Get the minimum number of threads in the created thread pool. + * + * @return The minimum number of threads + */ + default Integer getMinThreads() { + return null; + } + + /** + * Get the maximum number of threads in the created thread pool. + * + * @return The maximum number of threads + */ + default Integer getMaxThreads() { + return null; + } + } diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java index 407ef99ec..e11c5594c 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java @@ -51,6 +51,9 @@ public class MicronautServletConfiguration implements Named, ServletConfiguratio private boolean asyncSupported = true; private boolean enableVirtualThreads = true; + private Integer minThreads; + private Integer maxThreads; + /** * Default constructor. @@ -151,4 +154,32 @@ public void setAsyncFileServingEnabled(boolean enabled) { public boolean isAsyncFileServingEnabled() { return asyncSupported && asyncFileServingEnabled; } + + @Override + public Integer getMinThreads() { + return minThreads; + } + + /** + * Specify the minimum number of threads in the created thread pool. + * + * @param minThreads The minimum number of threads + */ + public void setMinThreads(Integer minThreads) { + this.minThreads = minThreads; + } + + @Override + public Integer getMaxThreads() { + return maxThreads; + } + + /** + * Specify the maximum number of threads in the created thread pool. + * + * @param maxThreads The maximum number of threads + */ + public void setMaxThreads(Integer maxThreads) { + this.maxThreads = maxThreads; + } } From 5dc0681cbee7e9a7cf593f5730c1e6f3764ce543 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 19:53:30 +0200 Subject: [PATCH 034/180] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.5.1 (#724) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4847c02cf..fbd7729bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.5.0" +micronaut = "4.5.1" micronaut-docs = "2.0.0" micronaut-test = "4.0.1" From 4e71bf84a5377fe546e101f46633d4b6abb14d5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 19:53:44 +0200 Subject: [PATCH 035/180] fix(deps): update dependency io.micronaut.serde:micronaut-serde-bom to v2.10.1 (#723) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fbd7729bf..c72a31e66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ managed-jetty = '11.0.21' micronaut-reactor = "3.3.0" micronaut-security = "4.8.0" -micronaut-serde = "2.10.0" +micronaut-serde = "2.10.1" micronaut-session = "4.3.0" micronaut-validation = "4.5.0" google-cloud-functions = '1.1.0' From 6127aec163a9982de340a46447dc85654af83e5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 19:54:02 +0200 Subject: [PATCH 036/180] fix(deps): update dependency io.micronaut.validation:micronaut-validation-bom to v4.6.0 (#726) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c72a31e66..a2304b43c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ micronaut-reactor = "3.3.0" micronaut-security = "4.8.0" micronaut-serde = "2.10.1" micronaut-session = "4.3.0" -micronaut-validation = "4.5.0" +micronaut-validation = "4.6.0" google-cloud-functions = '1.1.0' kotlin = "1.9.24" micronaut-logging = "1.3.0" From 944ea6fa39a6b5e8e0821082338622a284a3a943 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 19:54:15 +0200 Subject: [PATCH 037/180] fix(deps): update dependency io.micronaut.security:micronaut-security-bom to v4.9.0 (#725) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2304b43c..79b87afae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ bcpkix = "1.70" managed-jetty = '11.0.21' micronaut-reactor = "3.3.0" -micronaut-security = "4.8.0" +micronaut-security = "4.9.0" micronaut-serde = "2.10.1" micronaut-session = "4.3.0" micronaut-validation = "4.6.0" From 2742643cb4489d6477fa62b70e1c475a44cc58b4 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 1 Jun 2024 11:38:41 +0000 Subject: [PATCH 038/180] [skip ci] Release v4.9.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6c0a6accc..e8400efd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.8.1-SNAPSHOT +projectVersion=4.9.0 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 57b956c2952fade0d6d93e84df646738462dd2da Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Sat, 1 Jun 2024 11:42:24 +0000 Subject: [PATCH 039/180] chore: Bump version to 4.9.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e8400efd1..f31da9a80 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.0 +projectVersion=4.9.1-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From 1a73c50eed0981f729cff435e12d250dbcf012ab Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:43:38 +0200 Subject: [PATCH 040/180] Update common files (#729) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 065ee5620..5afb151f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,7 +134,7 @@ jobs: actions: read # To read the workflow path. id-token: write # To sign the provenance. contents: write # To add assets to a release. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: "${{ needs.provenance-subject.outputs.artifacts-sha256 }}" upload-assets: true # Upload to a new release. From e5a4e85506845dfd2138d4020346895f941d4ba2 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:57:17 +0200 Subject: [PATCH 041/180] Update common files (#730) --- .github/renovate.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index ed525891b..09c2a5983 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,7 +12,15 @@ "packageRules": [ { "matchPackagePatterns": ["actions.*"], - "dependencyDashboardApproval": true + "dependencyDashboardApproval": true, + "matchUpdateTypes": ["patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true + }, + { + "matchUpdateTypes": ["patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true } ] } From ced1b4a4fad112300588d672f304c39676fdb696 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Tue, 4 Jun 2024 18:13:37 +0200 Subject: [PATCH 042/180] Fix reactive error handling during write (#731) In the azure module, it is not permitted to set HTTP headers/status after getOutputStream is called. This becomes a problem when returning Flux.error from a controller, because the previous code would call getOutputStream (and write a json array start) before the error is seen. This change delays calling getOutputStream until after the first element is taken from the reactive stream. Also adjusted the comment on AvailableByteArrayBody and InputStreamByteBody. --- .../servlet/http/ServletHttpHandler.java | 67 +++++++++++++++++-- .../http/body/AvailableByteArrayBody.java | 2 +- .../http/body/InputStreamByteBody.java | 2 + 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index 93d4779f9..2e1db40d1 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -61,6 +61,7 @@ import java.io.EOFException; import java.io.File; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URI; @@ -448,15 +449,19 @@ private void encodeResponse(ServletExchange exchange, return; } else { // fallback to blocking - try (OutputStream outputStream = servletResponse.getOutputStream()) { + + // LazyOutputStream must not be initialized before publisher exceptions + // are checked + try (OutputStream outputStream = new LazyOutputStream(servletResponse)) { boolean json = mediaType.equals(MediaType.APPLICATION_JSON_TYPE); - if (json) { - outputStream.write('['); - } boolean first = true; for (Object o : Flux.from(publisher).toIterable()) { - if (!first && json) { - outputStream.write(','); + if (json) { + if (!first) { + outputStream.write(','); + } else { + outputStream.write('['); + } } first = false; @@ -465,10 +470,13 @@ private void encodeResponse(ServletExchange exchange, mediaType, o, response.getHeaders(), - outputStream + new UncloseableOutputStream(outputStream) ); } if (json) { + if (first) { + outputStream.write('['); + } outputStream.write(']'); } } catch (IOException e) { @@ -547,4 +555,49 @@ protected FileCustomizableResponseType findFile(HttpRequest request) { return matchFile(request.getPath()).orElse(null); } } + + private static final class LazyOutputStream extends OutputStream { + private ServletHttpResponse response; + private OutputStream stream; + + public LazyOutputStream(ServletHttpResponse response) { + this.response = response; + } + + private OutputStream stream() throws IOException { + if (stream == null) { + stream = response.getOutputStream(); + response = null; + } + return stream; + } + + @Override + public void write(int b) throws IOException { + stream().write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + stream().write(b, off, len); + } + + @Override + public void close() throws IOException { + if (stream != null) { + stream.close(); + } + } + } + + private static final class UncloseableOutputStream extends FilterOutputStream { + public UncloseableOutputStream(OutputStream out) { + super(out); + } + + @Override + public void close() throws IOException { + // do nothing + } + } } diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java index ba6e3a049..afeb02add 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java @@ -28,7 +28,7 @@ /** * {@link io.micronaut.http.body.AvailableByteBody} implementation based on a byte array. *

- * Note: While internal, this is also used from the AWS and GCP modules. + * Note: While internal, this is also used from the Azure, AWS and GCP modules. * * @author Jonas Konrad * @since 4.9.0 diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java index 1417133b2..2a3d32fbe 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java @@ -36,6 +36,8 @@ /** * Streaming {@link io.micronaut.http.body.ByteBody} implementation for servlet. + *

+ * Note: While internal, this is also used from the Azure, AWS and GCP modules. * * @since 4.9.0 * @author Jonas Konrad From 6eb53f434d3624dbd83260b3a012e3d5f0353400 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 4 Jun 2024 18:43:49 +0000 Subject: [PATCH 043/180] [skip ci] Release v4.9.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f31da9a80..9d7b85475 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.1-SNAPSHOT +projectVersion=4.9.1 projectGroup=io.micronaut.servlet title=Micronaut Servlet From d49c305357ed324ef2f176aabd1293896202ae9a Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 4 Jun 2024 18:48:39 +0000 Subject: [PATCH 044/180] chore: Bump version to 4.9.2-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9d7b85475..7b9ef17ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.1 +projectVersion=4.9.2-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From c052907bc4e144686ecbdbc9ccbf7e327fa8a340 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Sat, 8 Jun 2024 20:00:45 +0200 Subject: [PATCH 045/180] Update common files (#733) --- .github/workflows/gradle.yml | 2 +- .github/workflows/release.yml | 10 ++++------ gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 78142de20..33a347c60 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -78,7 +78,7 @@ jobs: - name: "📜 Upload binary compatibility check results" if: matrix.java == '17' - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5afb151f4..5a18844dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,13 +66,13 @@ jobs: # Store the hash in a file, which is uploaded as a workflow artifact. sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256 - name: Upload build artifacts - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: gradle-build-outputs path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* retention-days: 5 - name: Upload artifacts-sha256 - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: artifacts-sha256 path: artifacts-sha256 @@ -115,7 +115,7 @@ jobs: artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} steps: - name: Download artifacts-sha256 - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: artifacts-sha256 # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job @@ -148,9 +148,7 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download artifacts - # Important: update actions/download-artifact to v4 only when generator_generic_slsa3.yml is also compatible. - # See https://github.com/slsa-framework/slsa-github-generator/issues/3068 - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: gradle-build-outputs path: build/repo diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..a4413138c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..b740cf133 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. From 7211c087330c422a2d94f5e51944570b435cdb8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 08:57:33 +0200 Subject: [PATCH 046/180] chore(deps): update plugin io.micronaut.build.shared.settings to v7.1.1 (#735) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index d8359c338..2eccd404c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.0.1' + id 'io.micronaut.build.shared.settings' version '7.1.1' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From c788d0a84dbd7151751bc0c82c5e28ea0bde0540 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 08:57:46 +0200 Subject: [PATCH 047/180] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.5.3 (#734) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79b87afae..17c29aedb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.5.1" +micronaut = "4.5.3" micronaut-docs = "2.0.0" micronaut-test = "4.0.1" From 56776af8221f360c39d8e792ad4415e37dce2cae Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk <80816836+andriy-dmytruk@users.noreply.github.com> Date: Sun, 16 Jun 2024 02:36:37 -0400 Subject: [PATCH 048/180] Allow empty response when an exception is thrown (#737) --- .../servlet/http/ServletHttpHandler.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index 2e1db40d1..9f055eef8 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -215,11 +215,16 @@ public void service(ServletExchange exchange) { if (exchange.getRequest().isAsyncSupported()) { exchange.getRequest().executeAsync(asyncExecution -> { try (PropagatedContext.Scope ignore = PropagatedContext.getOrEmpty().plus(new ServerHttpRequestContext(req)).propagate()) { - lc.handleNormal(req) - .onComplete((response, throwable) -> onComplete(exchange, req, response.toMutableResponse(), throwable, httpResponse -> { - asyncExecution.complete(); - requestTerminated.accept(httpResponse); - })); + lc.handleNormal(req).onComplete((response, throwable) -> onComplete( + exchange, + req, + response == null ? null : response.toMutableResponse(), + throwable, + httpResponse -> { + asyncExecution.complete(); + requestTerminated.accept(httpResponse); + } + )); } }); } else { @@ -228,7 +233,13 @@ public void service(ServletExchange exchange) { lc.handleNormal(req) .onComplete((response, throwable) -> { try { - onComplete(exchange, req, response.toMutableResponse(), throwable, requestTerminated); + onComplete( + exchange, + req, + response == null ? null : response.toMutableResponse(), + throwable, + requestTerminated + ); } finally { termination.complete(null); } From 92d3e3141f1a0472aa7ec1a389917016640731e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:33:44 +0200 Subject: [PATCH 049/180] fix(deps): update dependency io.undertow:undertow-servlet to v2.3.14.final (#747) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17c29aedb..102250253 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' kotest-runner = '5.8.1' -undertow = '2.3.13.Final' +undertow = '2.3.14.Final' tomcat = '10.1.24' graal-svm = "23.1.2" bcpkix = "1.70" From ca0ad5e0c3748f4f0fab8373eaa4467a54e62c20 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:40:55 +0200 Subject: [PATCH 050/180] Update common files (#744) --- .github/workflows/gradle.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 33a347c60..d91a8e4fe 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,14 +45,14 @@ jobs: fetch-depth: 0 - name: "🔧 Setup GraalVM CE" - uses: graalvm/setup-graalvm@v1.2.1 + uses: graalvm/setup-graalvm@v1.2.2 with: distribution: 'graalvm' java-version: ${{ matrix.java }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.3.2 + uses: gradle/gradle-build-action@v3.4.2 - name: "❓ Optional setup step" run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a18844dd..f234f585b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -146,7 +146,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Download artifacts uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: From af19e205acc4de644d2fddc0018eea08323170cf Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 25 Jun 2024 10:47:48 +0200 Subject: [PATCH 051/180] fix web-fragment.xml syntax (#750) --- servlet-engine/src/main/resources/META-INF/web-fragment.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/servlet-engine/src/main/resources/META-INF/web-fragment.xml b/servlet-engine/src/main/resources/META-INF/web-fragment.xml index caced2535..899238053 100644 --- a/servlet-engine/src/main/resources/META-INF/web-fragment.xml +++ b/servlet-engine/src/main/resources/META-INF/web-fragment.xml @@ -3,7 +3,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd" version="3.0" metadata-complete="true"> - - micronaut_servlet - + micronaut_servlet From ac079c8fb815686ffe772e44ad6d3d058f8668c8 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 25 Jun 2024 08:48:47 +0000 Subject: [PATCH 052/180] [skip ci] Release v4.9.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7b9ef17ad..e3b9b633e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.2-SNAPSHOT +projectVersion=4.9.2 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 2347b8b0fff25b6141b440631eccdb37ecfad67b Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 25 Jun 2024 08:54:06 +0000 Subject: [PATCH 053/180] chore: Bump version to 4.9.3-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e3b9b633e..e785749f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.2 +projectVersion=4.9.3-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From d1d37045c8ae68f0fc76546f02e21f2d4cd835cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:10:27 +0200 Subject: [PATCH 054/180] fix(deps): update managed.jetty to v11.0.22 (#752) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 102250253..c4dd237d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ tomcat = '10.1.24' graal-svm = "23.1.2" bcpkix = "1.70" -managed-jetty = '11.0.21' +managed-jetty = '11.0.22' micronaut-reactor = "3.3.0" micronaut-security = "4.9.0" From 88b0cffc5ce33556f13ff3bfbdec9d4dd8891aa2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:10:40 +0200 Subject: [PATCH 055/180] fix(deps): update dependency org.apache.tomcat.embed:tomcat-embed-core to v10.1.25 (#751) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4dd237d4..57d03e360 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' kotest-runner = '5.8.1' undertow = '2.3.14.Final' -tomcat = '10.1.24' +tomcat = '10.1.25' graal-svm = "23.1.2" bcpkix = "1.70" From c71a7611f93222307ff742d08fcd95fed261994d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:10:54 +0200 Subject: [PATCH 056/180] chore(deps): update plugin io.micronaut.build.shared.settings to v7.1.4 (#738) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 2eccd404c..fee6199ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.1.1' + id 'io.micronaut.build.shared.settings' version '7.1.4' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From b565adf2f47d4d1939b037d0de033475f8067173 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:11:08 +0200 Subject: [PATCH 057/180] fix(deps): update dependency io.micronaut.validation:micronaut-validation-bom to v4.6.1 (#753) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57d03e360..4fe855a0c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ micronaut-reactor = "3.3.0" micronaut-security = "4.9.0" micronaut-serde = "2.10.1" micronaut-session = "4.3.0" -micronaut-validation = "4.6.0" +micronaut-validation = "4.6.1" google-cloud-functions = '1.1.0' kotlin = "1.9.24" micronaut-logging = "1.3.0" From 0e82ac956f2769163e02752be6c0e586535ac69d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:11:33 +0200 Subject: [PATCH 058/180] fix(deps): update dependency io.micronaut.serde:micronaut-serde-bom to v2.10.2 (#746) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4fe855a0c..6fa470dc1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ managed-jetty = '11.0.22' micronaut-reactor = "3.3.0" micronaut-security = "4.9.0" -micronaut-serde = "2.10.1" +micronaut-serde = "2.10.2" micronaut-session = "4.3.0" micronaut-validation = "4.6.1" google-cloud-functions = '1.1.0' From 3352ab9fb2901ea45f63620613dc1460506d9953 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:11:46 +0200 Subject: [PATCH 059/180] fix(deps): update dependency io.kotest:kotest-runner-junit5 to v5.9.1 (#719) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fa470dc1..314727304 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ groovy = "4.0.15" spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' -kotest-runner = '5.8.1' +kotest-runner = '5.9.1' undertow = '2.3.14.Final' tomcat = '10.1.25' graal-svm = "23.1.2" From e40377f0b6036339d1d162d3b070c38513dc0669 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 22 Jul 2024 14:31:25 +0000 Subject: [PATCH 060/180] [skip ci] Release v.9.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e785749f3..2b1b8d5f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.3-SNAPSHOT +projectVersion=.9.3 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 4a22c69547748d3bf9cdff190b292763ee09cd3d Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 22 Jul 2024 14:35:58 +0000 Subject: [PATCH 061/180] chore: Bump version to 9.3.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2b1b8d5f0..6ccb62424 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=.9.3 +projectVersion=9.3.1-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From 0f4727da9c9850620398f9682c4cb1e3d33c3b99 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:27:53 +0200 Subject: [PATCH 062/180] Update common files (#758) --- .github/workflows/gradle.yml | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 ++++- gradlew.bat | 2 ++ 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d91a8e4fe..8e1e3e37e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -52,7 +52,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.4.2 + uses: gradle/gradle-build-action@v3.5.0 - name: "❓ Optional setup step" run: | diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 zcmYLtRag{&)-BQ@Dc#cDDP2Q%r*wBHJ*0FE-92)X$3_b$L+F2Fa28UVeg>}yRjC}^a^+(Cdu_FTlV;w_x7ig{yd(NYi_;SHXEq`|Qa`qPMf1B~v#%<*D zn+KWJfX#=$FMopqZ>Cv7|0WiA^M(L@tZ=_Hi z*{?)#Cn^{TIzYD|H>J3dyXQCNy8f@~OAUfR*Y@C6r=~KMZ{X}q`t@Er8NRiCUcR=?Y+RMv`o0i{krhWT6XgmUt!&X=e_Q2=u@F=PXKpr9-FL@0 zfKigQcGHyPn{3vStLFk=`h@+Lh1XBNC-_nwNU{ytxZF$o}oyVfHMj|ZHWmEmZeNIlO5eLco<=RI&3=fYK*=kmv*75aqE~&GtAp(VJ z`VN#&v2&}|)s~*yQ)-V2@RmCG8lz5Ysu&I_N*G5njY`<@HOc*Bj)ZwC%2|2O<%W;M z+T{{_bHLh~n(rM|8SpGi8Whep9(cURNRVfCBQQ2VG<6*L$CkvquqJ~9WZ~!<6-EZ&L(TN zpSEGXrDiZNz)`CzG>5&_bxzBlXBVs|RTTQi5GX6s5^)a3{6l)Wzpnc|Cc~(5mO)6; z6gVO2Zf)srRQ&BSeg0)P2en#<)X30qXB{sujc3Ppm4*)}zOa)@YZ<%1oV9K%+(VzJ zk(|p>q-$v>lImtsB)`Mm;Z0LaU;4T1BX!wbnu-PSlH1%`)jZZJ(uvbmM^is*r=Y{B zI?(l;2n)Nx!goxrWfUnZ?y5$=*mVU$Lpc_vS2UyW>tD%i&YYXvcr1v7hL2zWkHf42 z_8q$Gvl>%468i#uV`RoLgrO+R1>xP8I^7~&3(=c-Z-#I`VDnL`6stnsRlYL zJNiI`4J_0fppF<(Ot3o2w?UT*8QQrk1{#n;FW@4M7kR}oW-}k6KNQaGPTs=$5{Oz} zUj0qo@;PTg#5moUF`+?5qBZ)<%-$qw(Z?_amW*X}KW4j*FmblWo@SiU16V>;nm`Eg zE0MjvGKN_eA%R0X&RDT!hSVkLbF`BFf;{8Nym#1?#5Fb?bAHY(?me2tww}5K9AV9y+T7YaqaVx8n{d=K`dxS|=))*KJn(~8u@^J% zj;8EM+=Dq^`HL~VPag9poTmeP$E`npJFh^|=}Mxs2El)bOyoimzw8(RQle(f$n#*v zzzG@VOO(xXiG8d?gcsp-Trn-36}+S^w$U(IaP`-5*OrmjB%Ozzd;jfaeRHAzc_#?- z`0&PVZANQIcb1sS_JNA2TFyN$*yFSvmZbqrRhfME3(PJ62u%KDeJ$ZeLYuiQMC2Sc z35+Vxg^@gSR6flp>mS|$p&IS7#fL@n20YbNE9(fH;n%C{w?Y0=N5?3GnQLIJLu{lm zV6h@UDB+23dQoS>>)p`xYe^IvcXD*6nDsR;xo?1aNTCMdbZ{uyF^zMyloFDiS~P7W>WuaH2+`xp0`!d_@>Fn<2GMt z&UTBc5QlWv1)K5CoShN@|0y1M?_^8$Y*U(9VrroVq6NwAJe zxxiTWHnD#cN0kEds(wN8YGEjK&5%|1pjwMH*81r^aXR*$qf~WiD2%J^=PHDUl|=+f zkB=@_7{K$Fo0%-WmFN_pyXBxl^+lLG+m8Bk1OxtFU}$fQU8gTYCK2hOC0sVEPCb5S z4jI07>MWhA%cA{R2M7O_ltorFkJ-BbmPc`{g&Keq!IvDeg8s^PI3a^FcF z@gZ2SB8$BPfenkFc*x#6&Z;7A5#mOR5qtgE}hjZ)b!MkOQ zEqmM3s>cI_v>MzM<2>U*eHoC69t`W`^9QBU^F$ z;nU4%0$)$ILukM6$6U+Xts8FhOFb|>J-*fOLsqVfB=vC0v2U&q8kYy~x@xKXS*b6i zy=HxwsDz%)!*T5Bj3DY1r`#@Tc%LKv`?V|g6Qv~iAnrqS+48TfuhmM)V_$F8#CJ1j4;L}TBZM~PX!88IT+lSza{BY#ER3TpyMqi# z#{nTi!IsLYt9cH?*y^bxWw4djrd!#)YaG3|3>|^1mzTuXW6SV4+X8sA2dUWcjH)a3 z&rXUMHbOO?Vcdf3H<_T-=DB0M4wsB;EL3lx?|T(}@)`*C5m`H%le54I{bfg7GHqYB z9p+30u+QXMt4z&iG%LSOk1uw7KqC2}ogMEFzc{;5x`hU(rh0%SvFCBQe}M#RSWJv;`KM zf7D&z0a)3285{R$ZW%+I@JFa^oZN)vx77y_;@p0(-gz6HEE!w&b}>0b)mqz-(lfh4 zGt}~Hl@{P63b#dc`trFkguB}6Flu!S;w7lp_>yt|3U=c|@>N~mMK_t#LO{n;_wp%E zQUm=z6?JMkuQHJ!1JV$gq)q)zeBg)g7yCrP=3ZA|wt9%_l#yPjsS#C7qngav8etSX+s?JJ1eX-n-%WvP!IH1%o9j!QH zeP<8aW}@S2w|qQ`=YNC}+hN+lxv-Wh1lMh?Y;LbIHDZqVvW^r;^i1O<9e z%)ukq=r=Sd{AKp;kj?YUpRcCr*6)<@Mnp-cx{rPayiJ0!7Jng}27Xl93WgthgVEn2 zQlvj!%Q#V#j#gRWx7((Y>;cC;AVbPoX*mhbqK*QnDQQ?qH+Q*$u6_2QISr!Fn;B-F@!E+`S9?+Jr zt`)cc(ZJ$9q^rFohZJoRbP&X3)sw9CLh#-?;TD}!i>`a;FkY6(1N8U-T;F#dGE&VI zm<*Tn>EGW(TioP@hqBg zn6nEolK5(}I*c;XjG!hcI0R=WPzT)auX-g4Znr;P`GfMa*!!KLiiTqOE*STX4C(PD z&}1K|kY#>~>sx6I0;0mUn8)=lV?o#Bcn3tn|M*AQ$FscYD$0H(UKzC0R588Mi}sFl z@hG4h^*;_;PVW#KW=?>N)4?&PJF&EO(X?BKOT)OCi+Iw)B$^uE)H>KQZ54R8_2z2_ z%d-F7nY_WQiSB5vWd0+>^;G^j{1A%-B359C(Eji{4oLT9wJ~80H`6oKa&{G- z)2n-~d8S0PIkTW_*Cu~nwVlE&Zd{?7QbsGKmwETa=m*RG>g??WkZ|_WH7q@ zfaxzTsOY2B3!Fu;rBIJ~aW^yqn{V;~4LS$xA zGHP@f>X^FPnSOxEbrnEOd*W7{c(c`b;RlOEQ*x!*Ek<^p*C#8L=Ty^S&hg zaV)g8<@!3p6(@zW$n7O8H$Zej+%gf^)WYc$WT{zp<8hmn!PR&#MMOLm^hcL2;$o=Q zXJ=9_0vO)ZpNxPjYs$nukEGK2bbL%kc2|o|zxYMqK8F?$YtXk9Owx&^tf`VvCCgUz zLNmDWtociY`(}KqT~qnVUkflu#9iVqXw7Qi7}YT@{K2Uk(Wx7Q-L}u^h+M(81;I*J ze^vW&-D&=aOQq0lF5nLd)OxY&duq#IdK?-r7En0MnL~W51UXJQFVVTgSl#85=q$+| zHI%I(T3G8ci9Ubq4(snkbQ*L&ksLCnX_I(xa1`&(Bp)|fW$kFot17I)jyIi06dDTTiI%gNR z8i*FpB0y0 zjzWln{UG1qk!{DEE5?0R5jsNkJ(IbGMjgeeNL4I9;cP&>qm%q7cHT}@l0v;TrsuY0 zUg;Z53O-rR*W!{Q*Gp26h`zJ^p&FmF0!EEt@R3aT4YFR0&uI%ko6U0jzEYk_xScP@ zyk%nw`+Ic4)gm4xvCS$)y;^)B9^}O0wYFEPas)!=ijoBCbF0DbVMP z`QI7N8;88x{*g=51AfHx+*hoW3hK(?kr(xVtKE&F-%Tb}Iz1Z8FW>usLnoCwr$iWv ztOVMNMV27l*fFE29x}veeYCJ&TUVuxsd`hV-8*SxX@UD6au5NDhCQ4Qs{{CJQHE#4 z#bg6dIGO2oUZQVY0iL1(Q>%-5)<7rhnenUjOV53*9Qq?aU$exS6>;BJqz2|#{We_| zX;Nsg$KS<+`*5=WA?idE6G~kF9oQPSSAs#Mh-|)@kh#pPCgp&?&=H@Xfnz`5G2(95 z`Gx2RfBV~`&Eyq2S9m1}T~LI6q*#xC^o*EeZ#`}Uw)@RD>~<_Kvgt2?bRbO&H3&h- zjB&3bBuWs|YZSkmcZvX|GJ5u7#PAF$wj0ULv;~$7a?_R%e%ST{al;=nqj-<0pZiEgNznHM;TVjCy5E#4f?hudTr0W8)a6o;H; zhnh6iNyI^F-l_Jz$F`!KZFTG$yWdioL=AhImGr!$AJihd{j(YwqVmqxMKlqFj<_Hlj@~4nmrd~&6#f~9>r2_e-^nca(nucjf z;(VFfBrd0?k--U9L*iey5GTc|Msnn6prtF*!5AW3_BZ9KRO2(q7mmJZ5kz-yms`04e; z=uvr2o^{lVBnAkB_~7b7?1#rDUh4>LI$CH1&QdEFN4J%Bz6I$1lFZjDz?dGjmNYlD zDt}f;+xn-iHYk~V-7Fx!EkS``+w`-f&Ow>**}c5I*^1tpFdJk>vG23PKw}FrW4J#x zBm1zcp^){Bf}M|l+0UjvJXRjP3~!#`I%q*E=>?HLZ>AvB5$;cqwSf_*jzEmxxscH; zcl>V3s>*IpK`Kz1vP#APs#|tV9~#yMnCm&FOllccilcNmAwFdaaY7GKg&(AKG3KFj zk@%9hYvfMO;Vvo#%8&H_OO~XHlwKd()gD36!_;o z*7pl*o>x9fbe?jaGUO25ZZ@#qqn@|$B+q49TvTQnasc$oy`i~*o}Ka*>Wg4csQOZR z|Fs_6-04vj-Dl|B2y{&mf!JlPJBf3qG~lY=a*I7SBno8rLRdid7*Kl@sG|JLCt60# zqMJ^1u^Gsb&pBPXh8m1@4;)}mx}m%P6V8$1oK?|tAk5V6yyd@Ez}AlRPGcz_b!c;; z%(uLm1Cp=NT(4Hcbk;m`oSeW5&c^lybx8+nAn&fT(!HOi@^&l1lDci*?L#*J7-u}} z%`-*V&`F1;4fWsvcHOlZF#SD&j+I-P(Mu$L;|2IjK*aGG3QXmN$e}7IIRko8{`0h9 z7JC2vi2Nm>g`D;QeN@^AhC0hKnvL(>GUqs|X8UD1r3iUc+-R4$=!U!y+?p6rHD@TL zI!&;6+LK_E*REZ2V`IeFP;qyS*&-EOu)3%3Q2Hw19hpM$3>v!!YABs?mG44{L=@rjD%X-%$ajTW7%t_$7to%9d3 z8>lk z?_e}(m&>emlIx3%7{ER?KOVXi>MG_)cDK}v3skwd%Vqn0WaKa1;e=bK$~Jy}p#~`B zGk-XGN9v)YX)K2FM{HNY-{mloSX|a?> z8Om9viiwL|vbVF~j%~hr;|1wlC0`PUGXdK12w;5Wubw}miQZ)nUguh?7asm90n>q= z;+x?3haT5#62bg^_?VozZ-=|h2NbG%+-pJ?CY(wdMiJ6!0ma2x{R{!ys=%in;;5@v z{-rpytg){PNbCGP4Ig>=nJV#^ie|N68J4D;C<1=$6&boh&ol~#A?F-{9sBL*1rlZshXm~6EvG!X9S zD5O{ZC{EEpHvmD5K}ck+3$E~{xrrg*ITiA}@ZCoIm`%kVqaX$|#ddV$bxA{jux^uRHkH)o6#}fT6XE|2BzU zJiNOAqcxdcQdrD=U7OVqer@p>30l|ke$8h;Mny-+PP&OM&AN z9)!bENg5Mr2g+GDIMyzQpS1RHE6ow;O*ye;(Qqej%JC?!D`u;<;Y}1qi5cL&jm6d9 za{plRJ0i|4?Q%(t)l_6f8An9e2<)bL3eULUVdWanGSP9mm?PqFbyOeeSs9{qLEO-) zTeH*<$kRyrHPr*li6p+K!HUCf$OQIqwIw^R#mTN>@bm^E=H=Ger_E=ztfGV9xTgh=}Hep!i97A;IMEC9nb5DBA5J#a8H_Daq~ z6^lZ=VT)7=y}H3=gm5&j!Q79#e%J>w(L?xBcj_RNj44r*6^~nCZZYtCrLG#Njm$$E z7wP?E?@mdLN~xyWosgwkCot8bEY-rUJLDo7gukwm@;TjXeQ>fr(wKP%7LnH4Xsv?o zUh6ta5qPx8a5)WO4 zK37@GE@?tG{!2_CGeq}M8VW(gU6QXSfadNDhZEZ}W2dwm)>Y7V1G^IaRI9ugWCP#sw1tPtU|13R!nwd1;Zw8VMx4hUJECJkocrIMbJI zS9k2|`0$SD%;g_d0cmE7^MXP_;_6`APcj1yOy_NXU22taG9Z;C2=Z1|?|5c^E}dR& zRfK2Eo=Y=sHm@O1`62ciS1iKv9BX=_l7PO9VUkWS7xlqo<@OxlR*tn$_WbrR8F?ha zBQ4Y!is^AIsq-46^uh;=9B`gE#Sh+4m>o@RMZFHHi=qb7QcUrgTos$e z^4-0Z?q<7XfCP~d#*7?hwdj%LyPj2}bsdWL6HctL)@!tU$ftMmV=miEvZ2KCJXP%q zLMG&%rVu8HaaM-tn4abcSE$88EYmK|5%_29B*L9NyO|~j3m>YGXf6fQL$(7>Bm9o zjHfJ+lmYu_`+}xUa^&i81%9UGQ6t|LV45I)^+m@Lz@jEeF;?_*y>-JbK`=ZVsSEWZ z$p^SK_v(0d02AyIv$}*8m)9kjef1-%H*_daPdSXD6mpc>TW`R$h9On=Z9n>+f4swL zBz^(d9uaQ_J&hjDvEP{&6pNz-bg;A===!Ac%}bu^>0}E)wdH1nc}?W*q^J2SX_A*d zBLF@n+=flfH96zs@2RlOz&;vJPiG6In>$&{D+`DNgzPYVu8<(N&0yPt?G|>D6COM# zVd)6v$i-VtYfYi1h)pXvO}8KO#wuF=F^WJXPC+;hqpv>{Z+FZTP1w&KaPl?D)*A=( z8$S{Fh;Ww&GqSvia6|MvKJg-RpNL<6MXTl(>1}XFfziRvPaLDT1y_tjLYSGS$N;8| zZC*Hcp!~u?v~ty3&dBm`1A&kUe6@`q!#>P>ZZZgGRYhNIxFU6B>@f@YL%hOV0=9s# z?@0~aR1|d9LFoSI+li~@?g({Y0_{~~E_MycHTXz`EZmR2$J$3QVoA25j$9pe?Ub)d z`jbm8v&V0JVfY-^1mG=a`70a_tjafgi}z-8$smw7Mc`-!*6y{rB-xN1l`G3PLBGk~ z{o(KCV0HEfj*rMAiluQuIZ1tevmU@m{adQQr3xgS!e_WXw&eE?GjlS+tL0@x%Hm{1 zzUF^qF*2KAxY0$~pzVRpg9dA*)^ z7&wu-V$7+Jgb<5g;U1z*ymus?oZi7&gr!_3zEttV`=5VlLtf!e&~zv~PdspA0JCRz zZi|bO5d)>E;q)?}OADAhGgey#6(>+36XVThP%b#8%|a9B_H^)Nps1md_lVv5~OO@(*IJO@;eqE@@(y}KA- z`zj@%6q#>hIgm9}*-)n(^Xbdp8`>w~3JCC`(H{NUh8Umm{NUntE+eMg^WvSyL+ilV zff54-b59jg&r_*;*#P~ON#I=gAW99hTD;}nh_j;)B6*tMgP_gz4?=2EJZg$8IU;Ly<(TTC?^)& zj@%V!4?DU&tE=8)BX6f~x0K+w$%=M3;Fpq$VhETRlJ8LEEe;aUcG;nBe|2Gw>+h7CuJ-^gYFhQzDg(`e=!2f7t0AXrl zAx`RQ1u1+}?EkEWSb|jQN)~wOg#Ss&1oHoFBvg{Z|4#g$)mNzjKLq+8rLR(jC(QUC Ojj7^59?Sdh$^Qpp*~F>< delta 8662 zcmYM1RaBhK(uL9BL4pT&ch}$qcL*As0R|^HFD`?-26qkaNwC3nu;A|Q0Yd)oJ7=x) z_f6HatE;=#>YLq{FoYf$!na@pfNwSyI%>|UMk5`vO(z@Ao)eZR(~D#FF?U$)+q)1q z9OVG^Ib0v?R8wYfQ*1H;5Oyixqnyt6cXR#u=LM~V7_GUu}N(b}1+x^JUL#_8Xj zB*(FInWvSPGo;K=k3}p&4`*)~)p`nX#}W&EpfKCcOf^7t zPUS81ov(mXS;$9To6q84I!tlP&+Z?lkctuIZ(SHN#^=JGZe^hr^(3d*40pYsjikBWME6IFf!!+kC*TBc!T)^&aJ#z0#4?OCUbNoa}pwh=_SFfMf|x$`-5~ zP%%u%QdWp#zY6PZUR8Mz1n$f44EpTEvKLTL;yiZrPCV=XEL09@qmQV#*Uu*$#-WMN zZ?rc(7}93z4iC~XHcatJev=ey*hnEzajfb|22BpwJ4jDi;m>Av|B?TqzdRm-YT(EV zCgl${%#nvi?ayAFYV7D_s#07}v&FI43BZz@`dRogK!k7Y!y6r=fvm~=F9QP{QTj>x z#Y)*j%`OZ~;rqP0L5@qYhR`qzh^)4JtE;*faTsB;dNHyGMT+fpyz~LDaMOO?c|6FD z{DYA+kzI4`aD;Ms|~h49UAvOfhMEFip&@&Tz>3O+MpC0s>`fl!T(;ZP*;Ux zr<2S-wo(Kq&wfD_Xn7XXQJ0E4u7GcC6pqe`3$fYZ5Eq4`H67T6lex_QP>Ca##n2zx z!tc=_Ukzf{p1%zUUkEO(0r~B=o5IoP1@#0A=uP{g6WnPnX&!1Z$UWjkc^~o^y^Kkn z%zCrr^*BPjcTA58ZR}?%q7A_<=d&<*mXpFSQU%eiOR`=78@}+8*X##KFb)r^zyfOTxvA@cbo65VbwoK0lAj3x8X)U5*w3(}5 z(Qfv5jl{^hk~j-n&J;kaK;fNhy9ZBYxrKQNCY4oevotO-|7X}r{fvYN+{sCFn2(40 zvCF7f_OdX*L`GrSf0U$C+I@>%+|wQv*}n2yT&ky;-`(%#^vF79p1 z>y`59E$f7!vGT}d)g)n}%T#-Wfm-DlGU6CX`>!y8#tm-Nc}uH50tG)dab*IVrt-TTEM8!)gIILu*PG_-fbnFjRA+LLd|_U3yas12Lro%>NEeG%IwN z{FWomsT{DqMjq{7l6ZECb1Hm@GQ`h=dcyApkoJ6CpK3n83o-YJnXxT9b2%TmBfKZ* zi~%`pvZ*;(I%lJEt9Bphs+j#)ws}IaxQYV6 zWBgVu#Kna>sJe;dBQ1?AO#AHecU~3cMCVD&G})JMkbkF80a?(~1HF_wv6X!p z6uXt_8u)`+*%^c@#)K27b&Aa%m>rXOcGQg8o^OB4t0}@-WWy38&)3vXd_4_t%F1|( z{z(S)>S!9eUCFA$fQ^127DonBeq@5FF|IR7(tZ?Nrx0(^{w#a$-(fbjhN$$(fQA(~|$wMG4 z?UjfpyON`6n#lVwcKQ+#CuAQm^nmQ!sSk>=Mdxk9e@SgE(L2&v`gCXv&8ezHHn*@% zi6qeD|I%Q@gb(?CYus&VD3EE#xfELUvni89Opq-6fQmY-9Di3jxF?i#O)R4t66ekw z)OW*IN7#{_qhrb?qlVwmM@)50jEGbjTiDB;nX{}%IC~pw{ev#!1`i6@xr$mgXX>j} zqgxKRY$fi?B7|GHArqvLWu;`?pvPr!m&N=F1<@i-kzAmZ69Sqp;$)kKg7`76GVBo{ zk+r?sgl{1)i6Hg2Hj!ehsDF3tp(@n2+l%ihOc7D~`vzgx=iVU0{tQ&qaV#PgmalfG zPj_JimuEvo^1X)dGYNrTHBXwTe@2XH-bcnfpDh$i?Il9r%l$Ob2!dqEL-To>;3O>` z@8%M*(1#g3_ITfp`z4~Z7G7ZG>~F0W^byMvwzfEf*59oM*g1H)8@2zL&da+$ms$Dp zrPZ&Uq?X)yKm7{YA;mX|rMEK@;W zA-SADGLvgp+)f01=S-d$Z8XfvEZk$amHe}B(gQX-g>(Y?IA6YJfZM(lWrf);5L zEjq1_5qO6U7oPSb>3|&z>OZ13;mVT zWCZ=CeIEK~6PUv_wqjl)pXMy3_46hB?AtR7_74~bUS=I}2O2CjdFDA*{749vOj2hJ z{kYM4fd`;NHTYQ_1Rk2dc;J&F2ex^}^%0kleFbM!yhwO|J^~w*CygBbkvHnzz@a~D z|60RVTr$AEa-5Z->qEMEfau=__2RanCTKQ{XzbhD{c!e5hz&$ZvhBX0(l84W%eW17 zQ!H)JKxP$wTOyq83^qmx1Qs;VuWuxclIp!BegkNYiwyMVBay@XWlTpPCzNn>&4)f* zm&*aS?T?;6?2>T~+!=Gq4fjP1Z!)+S<xiG>XqzY@WKKMzx?0|GTS4{ z+z&e0Uysciw#Hg%)mQ3C#WQkMcm{1yt(*)y|yao2R_FRX$WPvg-*NPoj%(k*{BA8Xx&0HEqT zI0Swyc#QyEeUc)0CC}x{p+J{WN>Z|+VZWDpzW`bZ2d7^Yc4ev~9u-K&nR zl#B0^5%-V4c~)1_xrH=dGbbYf*7)D&yy-}^V|Np|>V@#GOm($1=El5zV?Z`Z__tD5 zcLUi?-0^jKbZrbEny&VD!zA0Nk3L|~Kt4z;B43v@k~ zFwNisc~D*ZROFH;!f{&~&Pof-x8VG8{gSm9-Yg$G(Q@O5!A!{iQH0j z80Rs>Ket|`cbw>z$P@Gfxp#wwu;I6vi5~7GqtE4t7$Hz zPD=W|mg%;0+r~6)dC>MJ&!T$Dxq3 zU@UK_HHc`_nI5;jh!vi9NPx*#{~{$5Azx`_VtJGT49vB_=WN`*i#{^X`xu$9P@m>Z zL|oZ5CT=Zk?SMj{^NA5E)FqA9q88h{@E96;&tVv^+;R$K`kbB_ zZneKrSN+IeIrMq;4EcH>sT2~3B zrZf-vSJfekcY4A%e2nVzK8C5~rAaP%dV2Hwl~?W87Hdo<*EnDcbZqVUb#8lz$HE@y z2DN2AQh%OcqiuWRzRE>cKd)24PCc)#@o&VCo!Rcs;5u9prhK}!->CC)H1Sn-3C7m9 zyUeD#Udh1t_OYkIMAUrGU>ccTJS0tV9tW;^-6h$HtTbon@GL1&OukJvgz>OdY)x4D zg1m6Y@-|p;nB;bZ_O>_j&{BmuW9km4a728vJV5R0nO7wt*h6sy7QOT0ny-~cWTCZ3 z9EYG^5RaAbLwJ&~d(^PAiicJJs&ECAr&C6jQcy#L{JCK&anL)GVLK?L3a zYnsS$+P>UB?(QU7EI^%#9C;R-jqb;XWX2Bx5C;Uu#n9WGE<5U=zhekru(St>|FH2$ zOG*+Tky6R9l-yVPJk7giGulOO$gS_c!DyCog5PT`Sl@P!pHarmf7Y0HRyg$X@fB7F zaQy&vnM1KZe}sHuLY5u7?_;q!>mza}J?&eLLpx2o4q8$qY+G2&Xz6P8*fnLU+g&i2}$F%6R_Vd;k)U{HBg{+uuKUAo^*FRg!#z}BajS)OnqwXd!{u>Y&aH?)z%bwu_NB9zNw+~661!> zD3%1qX2{743H1G8d~`V=W`w7xk?bWgut-gyAl*6{dW=g_lU*m?fJ>h2#0_+J3EMz_ zR9r+0j4V*k>HU`BJaGd~@*G|3Yp?~Ljpth@!_T_?{an>URYtict~N+wb}%n)^GE8eM(=NqLnn*KJnE*v(7Oo)NmKB*qk;0&FbO zkrIQs&-)ln0-j~MIt__0pLdrcBH{C(62`3GvGjR?`dtTdX#tf-2qkGbeV;Ud6Dp0& z|A6-DPgg=v*%2`L4M&p|&*;;I`=Tn1M^&oER=Gp&KHBRxu_OuFGgX;-U8F?*2>PXjb!wwMMh_*N8$?L4(RdvV#O5cUu0F|_zQ#w1zMA4* zJeRk}$V4?zPVMB=^}N7x?(P7!x6BfI%*)yaUoZS0)|$bw07XN{NygpgroPW>?VcO} z@er3&#@R2pLVwkpg$X8HJM@>FT{4^Wi&6fr#DI$5{ERpM@|+60{o2_*a7k__tIvGJ9D|NPoX@$4?i_dQPFkx0^f$=#_)-hphQ93a0|`uaufR!Nlc^AP+hFWe~(j_DCZmv;7CJ4L7tWk{b;IFDvT zchD1qB=cE)Mywg5Nw>`-k#NQhT`_X^c`s$ODVZZ-)T}vgYM3*syn41}I*rz?)`Q<* zs-^C3!9AsV-nX^0wH;GT)Y$yQC*0x3o!Bl<%>h-o$6UEG?{g1ip>njUYQ}DeIw0@qnqJyo0do(`OyE4kqE2stOFNos%!diRfe=M zeU@=V=3$1dGv5ZbX!llJ!TnRQQe6?t5o|Y&qReNOxhkEa{CE6d^UtmF@OXk<_qkc0 zc+ckH8Knc!FTjk&5FEQ}$sxj!(a4223cII&iai-nY~2`|K89YKcrYFAMo^oIh@W^; zsb{KOy?dv_D5%}zPk_7^I!C2YsrfyNBUw_ude7XDc0-+LjC0!X_moHU3wmveS@GRu zX>)G}L_j1I-_5B|b&|{ExH~;Nm!xytCyc}Ed!&Hqg;=qTK7C93f>!m3n!S5Z!m`N} zjIcDWm8ES~V2^dKuv>8@Eu)Zi{A4;qHvTW7hB6B38h%$K76BYwC3DIQ0a;2fSQvo$ z`Q?BEYF1`@I-Nr6z{@>`ty~mFC|XR`HSg(HN>&-#&eoDw-Q1g;x@Bc$@sW{Q5H&R_ z5Aici44Jq-tbGnDsu0WVM(RZ=s;CIcIq?73**v!Y^jvz7ckw*=?0=B!{I?f{68@V( z4dIgOUYbLOiQccu$X4P87wZC^IbGnB5lLfFkBzLC3hRD?q4_^%@O5G*WbD?Wug6{<|N#Fv_Zf3ST>+v_!q5!fSy#{_XVq$;k*?Ar^R&FuFM7 zKYiLaSe>Cw@`=IUMZ*U#v>o5!iZ7S|rUy2(yG+AGnauj{;z=s8KQ(CdwZ>&?Z^&Bt z+74(G;BD!N^Ke>(-wwZN5~K%P#L)59`a;zSnRa>2dCzMEz`?VaHaTC>?&o|(d6e*Z zbD!=Ua-u6T6O!gQnncZ&699BJyAg9mKXd_WO8O`N@}bx%BSq)|jgrySfnFvzOj!44 z9ci@}2V3!ag8@ZbJO;;Q5ivdTWx+TGR`?75Jcje}*ufx@%5MFUsfsi%FoEx)&uzkN zgaGFOV!s@Hw3M%pq5`)M4Nz$)~Sr9$V2rkP?B7kvI7VAcnp6iZl zOd!(TNw+UH49iHWC4!W&9;ZuB+&*@Z$}>0fx8~6J@d)fR)WG1UndfdVEeKW=HAur| z15zG-6mf`wyn&x@&?@g1ibkIMob_`x7nh7yu9M>@x~pln>!_kzsLAY#2ng0QEcj)qKGj8PdWEuYKdM!jd{ zHP6j^`1g}5=C%)LX&^kpe=)X+KR4VRNli?R2KgYlwKCN9lcw8GpWMV+1Ku)~W^jV2 zyiTv-b*?$AhvU7j9~S5+u`Ysw9&5oo0Djp8e(j25Etbx42Qa=4T~}q+PG&XdkWDNF z7bqo#7KW&%dh~ST6hbu8S=0V`{X&`kAy@8jZWZJuYE}_#b4<-^4dNUc-+%6g($yN% z5ny^;ogGh}H5+Gq3jR21rQgy@5#TCgX+(28NZ4w}dzfx-LP%uYk9LPTKABaQh1ah) z@Y(g!cLd!Mcz+e|XI@@IH9z*2=zxJ0uaJ+S(iIsk7=d>A#L<}={n`~O?UTGX{8Pda z_KhI*4jI?b{A!?~-M$xk)w0QBJb7I=EGy&o3AEB_RloU;v~F8ubD@9BbxV1c36CsTX+wzAZlvUm*;Re06D+Bq~LYg-qF4L z5kZZ80PB&4U?|hL9nIZm%jVj0;P_lXar)NSt3u8xx!K6Y0bclZ%<9fwjZ&!^;!>ug zQ}M`>k@S{BR20cyVXtKK%Qa^7?e<%VSAPGmVtGo6zc6BkO5vW5)m8_k{xT3;ocdpH zudHGT06XU@y6U!&kP8i6ubMQl>cm7=(W6P7^24Uzu4Xpwc->ib?RSHL*?!d{c-aE# zp?TrFr{4iDL3dpljl#HHbEn{~eW2Nqfksa(r-}n)lJLI%e#Bu|+1% zN&!n(nv(3^jGx?onfDcyeCC*p6)DuFn_<*62b92Pn$LH(INE{z^8y?mEvvO zZ~2I;A2qXvuj>1kk@WsECq1WbsSC!0m8n=S^t3kxAx~of0vpv{EqmAmDJ3(o;-cvf zu$33Z)C0)Y4(iBhh@)lsS|a%{;*W(@DbID^$ z|FzcJB-RFzpkBLaFLQ;EWMAW#@K(D#oYoOmcctdTV?fzM2@6U&S#+S$&zA4t<^-!V z+&#*xa)cLnfMTVE&I}o#4kxP~JT3-A)L_5O!yA2ebq?zvb0WO1D6$r9p?!L0#)Fc> z+I&?aog~FPBH}BpWfW^pyc{2i8#Io6e)^6wv}MZn&`01oq@$M@5eJ6J^IrXLI) z4C!#kh)89u5*Q@W5(rYDqBKO6&G*kPGFZfu@J}ug^7!sC(Wcv3Fbe{$Sy|{-VXTct znsP+0v}kduRs=S=x0MA$*(7xZPE-%aIt^^JG9s}8$43E~^t4=MxmMts;q2$^sj=k( z#^suR{0Wl3#9KAI<=SC6hifXuA{o02vdyq>iw%(#tv+@ov{QZBI^*^1K?Q_QQqA5n9YLRwO3a7JR+1x3#d3lZL;R1@8Z!2hnWj^_5 z^M{3wg%f15Db5Pd>tS!6Hj~n^l478ljxe@>!C;L$%rKfm#RBw^_K&i~ZyY_$BC%-L z^NdD{thVHFlnwfy(a?{%!m;U_9ic*!OPxf&5$muWz7&4VbW{PP)oE5u$uXUZU>+8R zCsZ~_*HLVnBm*^{seTAV=iN)mB0{<}C!EgE$_1RMj1kGUU?cjSWu*|zFA(ZrNE(CkY7>Mv1C)E1WjsBKAE%w}{~apwNj z0h`k)C1$TwZ<3de9+>;v6A0eZ@xHm#^7|z9`gQ3<`+lpz(1(RsgHAM@Ja+)c?;#j- zC=&5FD)m@9AX}0g9XQ_Yt4YB}aT`XxM-t>7v@BV}2^0gu0zRH%S9}!P(MBAFGyJ8F zEMdB&{eGOd$RqV77Lx>8pX^<@TdL{6^K7p$0uMTLC^n)g*yXRXMy`tqjYIZ|3b#Iv z4<)jtQU5`b{A;r2QCqIy>@!uuj^TBed3OuO1>My{GQe<^9|$4NOHTKFp{GpdFY-kC zi?uHq>lF$}<(JbQatP0*>$Aw_lygfmUyojkE=PnV)zc)7%^5BxpjkU+>ol2}WpB2hlDP(hVA;uLdu`=M_A!%RaRTd6>Mi_ozLYOEh!dfT_h0dSsnQm1bk)%K45)xLw zql&fx?ZOMBLXtUd$PRlqpo2CxNQTBb=!T|_>p&k1F})Hq&xksq>o#4b+KSs2KyxPQ z#{(qj@)9r6u2O~IqHG76@Fb~BZ4Wz_J$p_NU9-b3V$$kzjN24*sdw5spXetOuU1SR z{v}b92c>^PmvPs>BK2Ylp6&1>tnPsBA0jg0RQ{({-?^SBBm>=W>tS?_h^6%Scc)8L zgsKjSU@@6kSFX%_3%Qe{i7Z9Wg7~fM_)v?ExpM@htI{G6Db5ak(B4~4kRghRp_7zr z#Pco0_(bD$IS6l2j>%Iv^Hc)M`n-vIu;-2T+6nhW0JZxZ|NfDEh;ZnAe d|9e8rKfIInFTYPwOD9TMuEcqhmizAn{|ERF)u#Xe diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138c..09523c0e5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf133..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30dbd..9d21a2183 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From b95cce2036f00acae5ba0cc1a76cf9063eb3d371 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:28:05 -0400 Subject: [PATCH 063/180] fix(deps): update dependency org.apache.tomcat.embed:tomcat-embed-core to v10.1.26 (#754) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 314727304..4d04bc33c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' kotest-runner = '5.9.1' undertow = '2.3.14.Final' -tomcat = '10.1.25' +tomcat = '10.1.26' graal-svm = "23.1.2" bcpkix = "1.70" From 549e6804ce0d0d2aa0a2702f1a614c6ec6cab012 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:28:43 -0400 Subject: [PATCH 064/180] fix(deps): update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.9.25 (#761) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d04bc33c..4bb79956f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ micronaut-serde = "2.10.2" micronaut-session = "4.3.0" micronaut-validation = "4.6.1" google-cloud-functions = '1.1.0' -kotlin = "1.9.24" +kotlin = "1.9.25" micronaut-logging = "1.3.0" # Micronaut From 29774a3ce28e2c36e9bc8799af2060b067f5adac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:28:58 -0400 Subject: [PATCH 065/180] fix(deps): update dependency io.micronaut.reactor:micronaut-reactor-bom to v3.4.1 (#736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4bb79956f..9db17c4c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ bcpkix = "1.70" managed-jetty = '11.0.22' -micronaut-reactor = "3.3.0" +micronaut-reactor = "3.4.1" micronaut-security = "4.9.0" micronaut-serde = "2.10.2" micronaut-session = "4.3.0" From 007176ebfccd5d0a034a6cb3b7f7a49181f11138 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:29:10 -0400 Subject: [PATCH 066/180] fix(deps): update dependency io.undertow:undertow-servlet to v2.3.15.final (#759) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9db17c4c4..91e888e9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.0.0' kotest-runner = '5.9.1' -undertow = '2.3.14.Final' +undertow = '2.3.15.Final' tomcat = '10.1.26' graal-svm = "23.1.2" bcpkix = "1.70" From cb31fb9a2c72f6916755f9c5844ee488465f82b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:29:22 -0400 Subject: [PATCH 067/180] fix(deps): update dependency io.micronaut.gradle:micronaut-gradle-plugin to v4.4.2 (#757) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 91e888e9c..ae2c3e6ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ kotlin = "1.9.25" micronaut-logging = "1.3.0" # Micronaut -micronaut-gradle-plugin = "4.4.0" +micronaut-gradle-plugin = "4.4.2" [libraries] # Core From b5f517612583c5a8e1a2ba0762a06d3b136d7e7a Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Mon, 5 Aug 2024 09:52:29 +0200 Subject: [PATCH 068/180] Don't use `DynamicMessageBodyWriter` (#765) --- .../micronaut/servlet/http/ServletHttpHandler.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index 9f055eef8..469ed4475 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -26,6 +26,7 @@ import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.Writable; import io.micronaut.core.propagation.PropagatedContext; +import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.http.HttpAttributes; @@ -380,7 +381,7 @@ private void encodeResponse(ServletExchange exchange, if (body != null && !isVoid) { Class bodyType = body.getClass(); - if (bodyArgument == null || !bodyArgument.isInstance(body)) { + if (bodyArgument == null || !bodyArgument.isInstance(body) || bodyArgument.getType().equals(Object.class)) { bodyArgument = (Argument) Argument.of(bodyType); } ServletResponseEncoder responseEncoder = (ServletResponseEncoder) responseEncoders.get(bodyType); @@ -421,7 +422,15 @@ private void encodeResponse(ServletExchange exchange, if (!(body instanceof HttpStatus)) { messageBodyWriter = routeInfoAttribute.map(RouteInfo::getMessageBodyWriter).orElse(null); if (messageBodyWriter == null) { - messageBodyWriter = new DynamicMessageBodyWriter(messageBodyHandlerRegistry, List.of(mediaType)); + MediaType finalMediaType = mediaType; + Argument finalBodyArgument = bodyArgument; + Optional> writer = messageBodyHandlerRegistry.findWriter(bodyArgument, List.of(mediaType)); + if (writer.isEmpty() && mediaType.equals(MediaType.TEXT_PLAIN_TYPE) && ClassUtils.isJavaBasicType(body.getClass())) { + // TODO: remove after Core 4.6 + writer = (Optional) messageBodyHandlerRegistry.findWriter(Argument.STRING, List.of(MediaType.TEXT_PLAIN_TYPE)); + } + messageBodyWriter = writer + .orElseThrow(() -> new CodecException("Cannot encode value of argument [" + finalBodyArgument + "]. No possible encoders found for media type: " + finalMediaType)); } } From 0534c1cc7c4ace97f3e962fb82d1c494cd662dcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 09:28:35 +0000 Subject: [PATCH 069/180] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.5.4 (#763) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae2c3e6ef..2e62dd24e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.5.3" +micronaut = "4.5.4" micronaut-docs = "2.0.0" micronaut-test = "4.0.1" From 31939f69aae1698776f99e7688470f61817ebd67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:06:39 +0000 Subject: [PATCH 070/180] fix(deps): update dependency io.micronaut.security:micronaut-security-bom to v4.9.1 (#762) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e62dd24e..9b759d899 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ bcpkix = "1.70" managed-jetty = '11.0.22' micronaut-reactor = "3.4.1" -micronaut-security = "4.9.0" +micronaut-security = "4.9.1" micronaut-serde = "2.10.2" micronaut-session = "4.3.0" micronaut-validation = "4.6.1" From 7166c5d5748926f888f2c6f34dc0d27dbb2d77e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:06:12 +0000 Subject: [PATCH 071/180] fix(deps): update dependency org.graalvm.nativeimage:svm to v23.1.4 (#760) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b759d899..ca6febc49 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ managed-servlet-api = '6.0.0' kotest-runner = '5.9.1' undertow = '2.3.15.Final' tomcat = '10.1.26' -graal-svm = "23.1.2" +graal-svm = "23.1.4" bcpkix = "1.70" managed-jetty = '11.0.22' From f121d84177e3c1d675cd260f4550a142887b5468 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 00:10:37 +0000 Subject: [PATCH 072/180] chore(deps): update graalvm/setup-graalvm action to v1.2.3 (#767) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8e1e3e37e..9442b6556 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,7 +45,7 @@ jobs: fetch-depth: 0 - name: "🔧 Setup GraalVM CE" - uses: graalvm/setup-graalvm@v1.2.2 + uses: graalvm/setup-graalvm@v1.2.3 with: distribution: 'graalvm' java-version: ${{ matrix.java }} From 6d60599176054543ac909a66d228374996271b70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:28:33 +0200 Subject: [PATCH 073/180] chore(deps): update plugin io.micronaut.build.shared.settings to v7.2.0 (#764) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index fee6199ed..d4ba2390f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.1.4' + id 'io.micronaut.build.shared.settings' version '7.2.0' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 50034ca33280ed9cf8e069405587245b8aea5f97 Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:28:55 +0200 Subject: [PATCH 074/180] Update common files (#766) --- .github/workflows/gradle.yml | 2 +- .github/workflows/release.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9442b6556..be653308d 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -78,7 +78,7 @@ jobs: - name: "📜 Upload binary compatibility check results" if: matrix.java == '17' - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f234f585b..9efeb2040 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,13 +66,13 @@ jobs: # Store the hash in a file, which is uploaded as a workflow artifact. sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256 - name: Upload build artifacts - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: gradle-build-outputs path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* retention-days: 5 - name: Upload artifacts-sha256 - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: artifacts-sha256 path: artifacts-sha256 @@ -115,7 +115,7 @@ jobs: artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} steps: - name: Download artifacts-sha256 - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: artifacts-sha256 # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job @@ -148,7 +148,7 @@ jobs: - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Download artifacts - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: gradle-build-outputs path: build/repo From 7c3a4560114c1a39ed7d95e2224df31ce255adf3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 14 Aug 2024 09:32:16 +0200 Subject: [PATCH 075/180] ci: projectVersion=4.9.3-SNAPSHOT [ci skip] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6ccb62424..e785749f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=9.3.1-SNAPSHOT +projectVersion=4.9.3-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From 6d15d7cd30af632dfbfd47d33b7aaf2a03cd4888 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 14 Aug 2024 09:36:10 +0200 Subject: [PATCH 076/180] ci: projectVersion=4.10.0-SNAPSHOT [ci skip] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e785749f3..cee64bdc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.9.3-SNAPSHOT +projectVersion=4.10.0-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From 21e4f638585cee89b95bc1afe79af59e333dadc8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:44:42 +0200 Subject: [PATCH 077/180] fix(deps): update dependency jakarta.servlet:jakarta.servlet-api to v6.1.0 (#739) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca6febc49..a6733274e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ micronaut-test = "4.0.1" groovy = "4.0.15" spock = "2.3-groovy-4.0" -managed-servlet-api = '6.0.0' +managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' undertow = '2.3.15.Final' tomcat = '10.1.26' From 59b3c87ccc36dc05fcff8f4073e4a53692bbafd8 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 16 Aug 2024 13:32:41 +0200 Subject: [PATCH 078/180] remove unused svm dependency (#769) --- .../io.micronaut.build.internal.servlet.implementation.gradle | 2 -- gradle/libs.versions.toml | 3 --- http-server-tomcat/tomcat.8080/logs/access_log.2024-08-14 | 3 +++ 3 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 http-server-tomcat/tomcat.8080/logs/access_log.2024-08-14 diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.servlet.implementation.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.servlet.implementation.gradle index 951e50ce9..db3dbf623 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.servlet.implementation.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.servlet.implementation.gradle @@ -7,8 +7,6 @@ dependencies { api(projects.micronautServletEngine) - compileOnly libs.graal.svm - testAnnotationProcessor mn.micronaut.inject.java testCompileOnly(mnValidation.micronaut.validation.processor) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6733274e..6eab0bb46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,6 @@ managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' undertow = '2.3.15.Final' tomcat = '10.1.26' -graal-svm = "23.1.4" bcpkix = "1.70" managed-jetty = '11.0.22' @@ -33,8 +32,6 @@ micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'mi boms-jetty = { module = 'org.eclipse.jetty:jetty-bom', version.ref = 'managed-jetty' } -graal-svm = { module = "org.graalvm.nativeimage:svm", version.ref = "graal-svm" } - managed-servlet-api = { module = 'jakarta.servlet:jakarta.servlet-api', version.ref = 'managed-servlet-api' } micronaut-logging = { module = "io.micronaut.logging:micronaut-logging-bom", version.ref = "micronaut-logging" } diff --git a/http-server-tomcat/tomcat.8080/logs/access_log.2024-08-14 b/http-server-tomcat/tomcat.8080/logs/access_log.2024-08-14 new file mode 100644 index 000000000..0eebbaaf2 --- /dev/null +++ b/http-server-tomcat/tomcat.8080/logs/access_log.2024-08-14 @@ -0,0 +1,3 @@ +#Fields: %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" +#Version: 2.0 +#Software: Apache Tomcat/10.1.26 From b52f4c296b978cf5555afc53abe8aac2c50c2583 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 19 Aug 2024 08:35:46 +0200 Subject: [PATCH 079/180] Move to core InputStreamByteBody (#770) Update to 4.6.x versions * Move to core InputStreamByteBody --- gradle/libs.versions.toml | 10 +-- .../servlet/jetty/JettyNotFoundSpec.groovy | 5 +- .../servlet/http/ByteArrayBufferFactory.java | 73 +++++++++++++++++++ .../servlet/http/ServletHttpHandler.java | 1 - .../http/body/AvailableByteArrayBody.java | 2 + .../http/body/InputStreamByteBody.java | 2 + .../engine/DefaultServletHttpRequest.java | 5 +- 7 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 servlet-core/src/main/java/io/micronaut/servlet/http/ByteArrayBufferFactory.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6eab0bb46..bb50092ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -micronaut = "4.5.4" +micronaut = "4.6.1" micronaut-docs = "2.0.0" -micronaut-test = "4.0.1" +micronaut-test = "4.4.0" groovy = "4.0.15" spock = "2.3-groovy-4.0" @@ -14,11 +14,11 @@ bcpkix = "1.70" managed-jetty = '11.0.22' -micronaut-reactor = "3.4.1" +micronaut-reactor = "3.5.0" micronaut-security = "4.9.1" -micronaut-serde = "2.10.2" +micronaut-serde = "2.11.0" micronaut-session = "4.3.0" -micronaut-validation = "4.6.1" +micronaut-validation = "4.7.0" google-cloud-functions = '1.1.0' kotlin = "1.9.25" micronaut-logging = "1.3.0" diff --git a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyNotFoundSpec.groovy b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyNotFoundSpec.groovy index 5f28a756c..5548ede60 100644 --- a/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyNotFoundSpec.groovy +++ b/http-server-jetty/src/test/groovy/io/micronaut/servlet/jetty/JettyNotFoundSpec.groovy @@ -6,17 +6,15 @@ import io.micronaut.context.annotation.Requires import io.micronaut.core.async.annotation.SingleResult import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType -import io.micronaut.http.annotation.Consumes import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.client.annotation.Client -import io.micronaut.test.extensions.spock.annotation.MicronautTest import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject import org.reactivestreams.Publisher import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import spock.lang.PendingFeature import spock.lang.Specification @MicronautTest @@ -32,7 +30,6 @@ class JettyNotFoundSpec extends Specification { Flux.from(client.streaming('notthere')).collectList().block() == [] } - @PendingFeature(reason = "https://github.com/micronaut-projects/micronaut-core/pull/9307") void "test 404 handling with not streaming publisher"() { when: def exists = Mono.from(client.mono('1234')).block() diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ByteArrayBufferFactory.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ByteArrayBufferFactory.java new file mode 100644 index 000000000..f2633d8fd --- /dev/null +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ByteArrayBufferFactory.java @@ -0,0 +1,73 @@ +/* + * 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.servlet.http; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.core.io.buffer.ByteBufferFactory; + +/** + * {@link ByteBufferFactory} implementation based on simple byte arrays. + * + * @since 4.10.0 + * @author Jonas Konrad + */ +@Internal +public class ByteArrayBufferFactory implements ByteBufferFactory { + public static final ByteArrayBufferFactory INSTANCE = new ByteArrayBufferFactory(); + + private ByteArrayBufferFactory() { + } + + @Override + public Void getNativeAllocator() { + throw new UnsupportedOperationException("No native allocator"); + } + + @Override + public ByteBuffer buffer() { + return buffer(0); + } + + @Override + public ByteBuffer buffer(int initialCapacity) { + return new ByteArrayByteBuffer<>(new byte[initialCapacity]); + } + + @Override + public ByteBuffer buffer(int initialCapacity, int maxCapacity) { + return buffer(initialCapacity); + } + + @Override + public ByteBuffer copiedBuffer(byte[] bytes) { + return wrap(bytes.clone()); + } + + @Override + public ByteBuffer copiedBuffer(java.nio.ByteBuffer nioBuffer) { + int pos = nioBuffer.position(); + int lim = nioBuffer.limit(); + byte[] arr = new byte[lim - pos]; + nioBuffer.get(pos, arr, 0, arr.length); + return wrap(arr); + } + + @Override + public ByteBuffer wrap(byte[] existing) { + return new ByteArrayByteBuffer<>(existing); + } +} diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index 469ed4475..e9c2d714d 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -38,7 +38,6 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Produces; -import io.micronaut.http.body.DynamicMessageBodyWriter; import io.micronaut.http.body.MessageBodyHandlerRegistry; import io.micronaut.http.body.MessageBodyWriter; import io.micronaut.http.codec.CodecException; diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java index afeb02add..350af77f1 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/AvailableByteArrayBody.java @@ -32,8 +32,10 @@ * * @author Jonas Konrad * @since 4.9.0 + * @deprecated Use {@link io.micronaut.http.body.stream.AvailableByteArrayBody} from core instead */ @Internal +@Deprecated(forRemoval = true) public final class AvailableByteArrayBody extends AbstractServletByteBody implements CloseableAvailableByteBody { private byte[] array; diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java b/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java index 2a3d32fbe..fd4a5f4cf 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/body/InputStreamByteBody.java @@ -41,8 +41,10 @@ * * @since 4.9.0 * @author Jonas Konrad + * @deprecated Use {@link io.micronaut.http.body.stream.InputStreamByteBody} from core instead */ @Internal +@Deprecated(forRemoval = true) public final class InputStreamByteBody extends AbstractServletByteBody { private final Context context; private ExtendedInputStream stream; diff --git a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java index 3d6abd0d7..5ea15fc56 100644 --- a/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java +++ b/servlet-engine/src/main/java/io/micronaut/servlet/engine/DefaultServletHttpRequest.java @@ -37,15 +37,16 @@ import io.micronaut.http.ServerHttpRequest; import io.micronaut.http.body.ByteBody; import io.micronaut.http.body.CloseableByteBody; +import io.micronaut.http.body.stream.InputStreamByteBody; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.cookie.Cookies; import io.micronaut.servlet.http.BodyBuilder; +import io.micronaut.servlet.http.ByteArrayBufferFactory; import io.micronaut.servlet.http.ParsedBodyHolder; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpRequest; import io.micronaut.servlet.http.ServletHttpResponse; import io.micronaut.servlet.http.StreamedServletMessage; -import io.micronaut.servlet.http.body.InputStreamByteBody; import jakarta.servlet.AsyncContext; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; @@ -135,7 +136,7 @@ protected DefaultServletHttpRequest(ConversionService conversionService, this.delegate = delegate; this.codecRegistry = codecRegistry; long contentLengthLong = delegate.getContentLengthLong(); - this.byteBody = InputStreamByteBody.create(new LazyDelegateInputStream(delegate), contentLengthLong < 0 ? OptionalLong.empty() : OptionalLong.of(contentLengthLong), ioExecutor); + this.byteBody = InputStreamByteBody.create(new LazyDelegateInputStream(delegate), contentLengthLong < 0 ? OptionalLong.empty() : OptionalLong.of(contentLengthLong), ioExecutor, ByteArrayBufferFactory.INSTANCE); String requestURI = delegate.getRequestURI(); From 20aebb92854c77b08404f500e3ebc4c9dd053249 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 19 Aug 2024 06:38:57 +0000 Subject: [PATCH 080/180] [skip ci] Release v4.10.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cee64bdc3..41396e022 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.10.0-SNAPSHOT +projectVersion=4.10.0 projectGroup=io.micronaut.servlet title=Micronaut Servlet From b9b375e1ac4d36712b463b92cf05692c3e473e41 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 19 Aug 2024 06:43:06 +0000 Subject: [PATCH 081/180] chore: Bump version to 4.10.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 41396e022..5bf62c858 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.10.0 +projectVersion=4.10.1-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From cdd7af653c7f2f37d56abdb9ac2ed7d3bcee411d Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:22:06 +0200 Subject: [PATCH 082/180] Update common files (#771) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9efeb2040..f4bfd84ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,6 +160,6 @@ jobs: - name: Upload assets # Upload the artifacts to the existing release. Note that the SLSA provenance will # attest to each artifact file and not the aggregated ZIP file. - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 with: files: artifacts.zip From 78f4c64328b558162dc7f302b203e47c1534a70f Mon Sep 17 00:00:00 2001 From: Sanjeeb Sahoo Date: Mon, 20 May 2024 16:47:15 -0700 Subject: [PATCH 083/180] GCN-4404: Initial commit of serverless micronaut web applications. This is currently using rawhttp as parser. I will change it soon to use a more widely used parser. I am able to test a sample Micronaut application with this module. The sample can be found in samples/ dir along with its own README and build script. I can see the app working both in interactive mode as well as inetd mode. Here's the plan before PR is submitted: 1. Use a more widely used parser. 2. Add test cases and samples for http-serverless module. 3. Fix TODOs in code. 4. Run compatibility tests for micronaut. 5. Update gcn/micronaut launcher to allow users to select the new serverless option. --- http-poja/README.md | 5 + http-poja/build.gradle | 11 + http-poja/samples/sample1/README.md | 7 + http-poja/samples/sample1/pom.xml | 208 +++++++++++++++++ .../http/poja/sample1/Application.java | 24 ++ .../http/poja/sample1/MyResource.java | 41 ++++ .../META-INF/native-image/reflect-config.json | 33 +++ .../native-image/resource-config.json | 16 ++ .../poja/RawHttpBasedServletHttpRequest.java | 216 ++++++++++++++++++ .../poja/RawHttpBasedServletHttpResponse.java | 120 ++++++++++ .../http/poja/ServerlessApplication.java | 128 +++++++++++ http-poja/src/test/resources/logback.xml | 16 ++ settings.gradle | 1 + 13 files changed, 826 insertions(+) create mode 100644 http-poja/README.md create mode 100644 http-poja/build.gradle create mode 100644 http-poja/samples/sample1/README.md create mode 100644 http-poja/samples/sample1/pom.xml create mode 100644 http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java create mode 100644 http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java create mode 100644 http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json create mode 100644 http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java create mode 100644 http-poja/src/test/resources/logback.xml diff --git a/http-poja/README.md b/http-poja/README.md new file mode 100644 index 000000000..759340844 --- /dev/null +++ b/http-poja/README.md @@ -0,0 +1,5 @@ +# Plain Old Java Application (POJA) using Micronaut HTTP Framework + +This 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). + diff --git a/http-poja/build.gradle b/http-poja/build.gradle new file mode 100644 index 000000000..1e29fd0b0 --- /dev/null +++ b/http-poja/build.gradle @@ -0,0 +1,11 @@ +plugins { + id("io.micronaut.build.internal.servlet.implementation") +} + +dependencies { + api project(":micronaut-servlet-core") + implementation("com.athaydes.rawhttp:rawhttp-core:2.4.1") + implementation("com.athaydes.rawhttp:rawhttp-cookies:0.2.1") + +} + diff --git a/http-poja/samples/sample1/README.md b/http-poja/samples/sample1/README.md new file mode 100644 index 000000000..8e1f5d332 --- /dev/null +++ b/http-poja/samples/sample1/README.md @@ -0,0 +1,7 @@ +export JAVA_HOME=`java_home v21` +mvn clean package native:compile +java -jar target/*.jar +GET / HTTP/1.1 +Host: h + + diff --git a/http-poja/samples/sample1/pom.xml b/http-poja/samples/sample1/pom.xml new file mode 100644 index 000000000..d35027b48 --- /dev/null +++ b/http-poja/samples/sample1/pom.xml @@ -0,0 +1,208 @@ + + 4.0.0 + + io.micronaut.samples + http-poja-sample1 + 1.0-SNAPSHOT + jar + + ${project.artifactId} + + + UTF-8 + 21 + 4.4.3 + 2.9.0 + 0.10.2 + io.micronaut.http.poja.sample1.Application + false + + true + + + + + + io.micronaut.platform + micronaut-platform + 4.4.2 + pom + import + + + io.micronaut.servlet + micronaut-http-poja + 4.2.0-SNAPSHOT + + + + + + + + io.micronaut + micronaut-inject + + + io.micronaut.serde + micronaut-serde-jackson + + + io.micronaut.servlet + micronaut-http-poja + + + + org.slf4j + slf4j-simple + + + + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + test + + + org.apache.httpcomponents + httpclient + 4.5.14 + test + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + io.micronaut.maven + micronaut-maven-plugin + + aot-${packaging}.properties + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 17 + 17 + true + + + + io.micronaut + micronaut-inject-java + ${micronaut.core.version} + + + + io.micronaut.serde + micronaut-serde-processor + ${micronaut.serialization.version} + + + io.micronaut + micronaut-inject + + + + + true + + -Amicronaut.processing.group=sahoo.graalos.progmodel.mn + -Amicronaut.processing.module=default + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + ${mainClass} + true + lib/ + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + + + ${artifactId}-${os.detected.classifier}-${version} + false + + + + --gc=serial + + --install-exit-handlers + + + ${nativeDryRun} + + ${quickBuild} + true + + + + + + diff --git a/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java b/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java new file mode 100644 index 000000000..b9107bb3d --- /dev/null +++ b/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java @@ -0,0 +1,24 @@ +package io.micronaut.http.poja.sample1; + +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) throws Exception { + // Need to disable banner because Micronaut prints banner to STDOUT, + // which gets mixed with HTTP response. + // See GCN-4489 + Micronaut.build(args) + .banner(false) + .mainClass(Application.class) + .start(); + Micronaut.run(Application.class, args); + } +} + diff --git a/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java b/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java new file mode 100644 index 000000000..e4848e9d5 --- /dev/null +++ b/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java @@ -0,0 +1,41 @@ +package io.micronaut.http.poja.sample1; + +import io.micronaut.core.annotation.NonNull; +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.Patch; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.annotation.Status; + +/** + * @author Sahoo. + */ +@Controller(value = "/", produces = MediaType.TEXT_PLAIN, consumes = MediaType.ALL) +public class MyResource { + @Get + public String index() { + return "Hello, Micronaut Without Netty!\n"; + } + + @Delete + public void delete() { + System.err.println("Delete called"); + } + + @Post("/{name}") + @Status(HttpStatus.CREATED) + public String create(@NonNull String name) { + return "Hello, " + name + "\n"; + } + + @Put("/{name}") + @Status(HttpStatus.OK) + public String update(@NonNull String name) { + return "Hello, " + name + "!\n"; + } +} + diff --git a/http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json b/http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000..2b5826e46 --- /dev/null +++ b/http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,33 @@ +[ + { + "name": "io.micronaut.http.poja.sample1.$MyResource$Definition", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "java.security.SecureRandomParameters" + }, + { + "name": "sun.security.provider.NativePRNG$NonBlocking", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "java.security.SecureRandomParameters" + ] + } + ] + }, + { + "name": "java.io.FileDescriptor", + "allDeclaredConstructors": true + } +] diff --git a/http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json b/http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 000000000..384c2ef21 --- /dev/null +++ b/http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,16 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\QMETA-INF/services/java.nio.file.spi.FileSystemProvider\\E" + }, + { + "pattern": "\\QMETA-INF/services/rawhttp.core.body.encoding.HttpMessageDecoder\\E" + }, + { + "pattern": "\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E" + } + ] + }, + "bundles": [] +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java new file mode 100644 index 000000000..5ed301bbe --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java @@ -0,0 +1,216 @@ +package io.micronaut.http.poja; + +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.MutableConvertibleValues; +import io.micronaut.core.convert.value.MutableConvertibleValuesMap; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpParameters; +import io.micronaut.http.cookie.Cookies; +import io.micronaut.servlet.http.ServletHttpRequest; +import rawhttp.cookies.ServerCookieHelper; +import rawhttp.core.RawHttp; +import rawhttp.core.RawHttpHeaders; +import rawhttp.core.RawHttpRequest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Sahoo. + */ +class RawHttpBasedServletHttpRequest implements ServletHttpRequest { + private final RawHttp rawHttp; + private final InputStream in; + private final RawHttpRequest rawHttpRequest; + private final RawHttpBasedHeaders headers; + + private final RawHttpBasedParameters queryParameters; + + public RawHttpBasedServletHttpRequest(InputStream in, ConversionService conversionService) { + this.rawHttp = new RawHttp(); + this.in = in; + try { + rawHttpRequest = rawHttp.parseRequest(in); +// System.err.println("DEBUG: Parsed following request from input stream: \n" + rawHttpRequest); + + } catch (IOException e) { + throw new RuntimeException(e); + } + headers = new RawHttpBasedHeaders(rawHttpRequest.getHeaders(), conversionService); + queryParameters = new RawHttpBasedParameters(getUri().getRawQuery(), conversionService); + } + + @Override + public InputStream getInputStream() throws IOException { + return null; + } + + @Override + public BufferedReader getReader() throws IOException { + return null; +// return new BufferedReader(new InputStreamReader(getInputStream(), +// rawHttp.getOptions().getHttpHeadersOptions().getHeaderValuesCharset())); + } + + @Override + public RawHttpRequest getNativeRequest() { + return rawHttpRequest; + } + + @Override + public @NonNull Cookies getCookies() { +// var cookies = ServerCookieHelper.readClientCookies(rawHttpRequest); + // TODO + throw new UnsupportedOperationException("TBD"); + } + + @Override + public @NonNull HttpParameters getParameters() { + return queryParameters; + } + + @Override + public @NonNull HttpMethod getMethod() { + return HttpMethod.parse(rawHttpRequest.getMethod()); + } + + @Override + public @NonNull URI getUri() { + return rawHttpRequest.getUri(); + } + + @Override + public @NonNull HttpHeaders getHeaders() { + return headers; + } + + @Override + public @NonNull MutableConvertibleValues getAttributes() { + // Attributes are used for sharing internal data and is not applicable in our case. + // So, return empty map. + return new MutableConvertibleValuesMap<>(); + } + + @Override + public @NonNull Optional getBody() { + // TODO: figure out what needs to be done. + System.err.println("TBD: getBody() Retuning null body for now."); + Thread.dumpStack(); + return Optional.empty(); + } + + public static class RawHttpBasedHeaders implements HttpHeaders { + private final RawHttpHeaders rawHttpHeaders; + private final ConversionService conversionService; + + private RawHttpBasedHeaders(RawHttpHeaders rawHttpHeaders, ConversionService conversionService) { + this.rawHttpHeaders = rawHttpHeaders; + this.conversionService = conversionService; + } + + @Override + public List getAll(CharSequence name) { + return rawHttpHeaders.get(String.valueOf(name)); + } + + @Override + public @Nullable String get(CharSequence name) { + List all = getAll(name); + return all.isEmpty() ? null : all.get(0); + } + + @Override + public Set names() { + return rawHttpHeaders.getUniqueHeaderNames(); + } + + @Override + public Collection> values() { + return rawHttpHeaders.asMap().values(); + } + + @Override + public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { + String header = get(name); + return header == null ? Optional.empty() : conversionService.convert(header, conversionContext); + } + } + + private static class RawHttpBasedParameters implements HttpParameters { + private final Map> queryParams; + private final ConversionService conversionService; + + private RawHttpBasedParameters(String queryString, ConversionService conversionService) { + queryParams = QueryParametersParser.parseQueryParameters(queryString); + this.conversionService = conversionService; + } + + @Override + public List getAll(CharSequence name) { + return queryParams.get(name.toString()); + } + + @Override + public @Nullable String get(CharSequence name) { + List all = getAll(name); + return all.isEmpty() ? null : all.get(0); + } + + @Override + public Set names() { + return queryParams.keySet(); + } + + @Override + public Collection> values() { + return queryParams.values(); + } + + @Override + public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { + String header = get(name); + return header == null ? Optional.empty() : conversionService.convert(header, conversionContext); + } + + static class QueryParametersParser { + public static Map> parseQueryParameters(String queryParameters) { + return queryParameters != null && !queryParameters.isEmpty() ? + Arrays.stream(queryParameters.split("[&;]")) + .map(QueryParametersParser::splitQueryParameter) + .collect(Collectors.groupingBy(Map.Entry::getKey, + LinkedHashMap::new, + Collectors.mapping(Map.Entry::getValue, Collectors.toList()))) : + Collections.emptyMap(); + } + + private static Map.Entry splitQueryParameter(String parameter) { + int idx = parameter.indexOf("="); + String key = decode(idx > 0 ? parameter.substring(0, idx) : parameter); + String value = idx > 0 && parameter.length() > idx + 1 ? decode(parameter.substring(idx + 1)) : ""; + return new AbstractMap.SimpleImmutableEntry<>(key, value); + } + + private static String decode(String urlEncodedString) { + return URLDecoder.decode(urlEncodedString, StandardCharsets.UTF_8); + } + } + } +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java new file mode 100644 index 000000000..d8ba236ff --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java @@ -0,0 +1,120 @@ +package io.micronaut.http.poja; + +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.simple.SimpleHttpHeaders; +import io.micronaut.servlet.http.ServletHttpResponse; +import rawhttp.core.HttpVersion; +import rawhttp.core.RawHttpHeaders; +import rawhttp.core.RawHttpResponse; +import rawhttp.core.StatusLine; +import rawhttp.core.body.EagerBodyReader; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Optional; + +/** + * @author Sahoo. + */ +class RawHttpBasedServletHttpResponse implements ServletHttpResponse, String> { + + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + private int code = HttpStatus.OK.getCode(); + + private String reason = HttpStatus.OK.getReason(); + + private String body; + private final SimpleHttpHeaders headers; + + private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); + + public RawHttpBasedServletHttpResponse(ConversionService conversionService) { + this.headers = new SimpleHttpHeaders(conversionService); + } + + @Override + public RawHttpResponse getNativeResponse() { + headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size())); + return new RawHttpResponse<>(null, + null, + new StatusLine(HttpVersion.HTTP_1_1, code, reason), + toRawHttpheaders(), + new EagerBodyReader(out.toByteArray())); + } + + private RawHttpHeaders toRawHttpheaders() { + RawHttpHeaders.Builder builder = RawHttpHeaders.newBuilder(); + headers.forEachValue(builder::with); + return builder.build(); + } + + @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 T body) { + // TODO + throw new UnsupportedOperationException("TBD"); + } + + @Override + public MutableHttpResponse status(int code, CharSequence message) { + this.code = code; + if (message == null) { + this.reason = HttpStatus.getDefaultReason(code); + } else { + this.reason = message.toString(); + } + return this; + } + + @Override + public int code() { + return code; + } + + @Override + public String reason() { + return reason; + } + + @Override + public MutableHttpHeaders getHeaders() { + return headers; + } + + @Override + public @NonNull MutableConvertibleValues getAttributes() { + return attributes; + } + + @Override + public @NonNull Optional getBody() { + return Optional.ofNullable(body); + } +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java new file mode 100644 index 000000000..5f4587358 --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -0,0 +1,128 @@ +package io.micronaut.http.poja; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.DefaultMutableConversionService; +import io.micronaut.runtime.ApplicationConfiguration; +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.servlet.http.ServletExchange; +import io.micronaut.servlet.http.ServletHttpHandler; +import io.micronaut.servlet.http.ServletHttpRequest; +import io.micronaut.servlet.http.ServletHttpResponse; +import jakarta.inject.Singleton; +import rawhttp.core.RawHttpRequest; +import rawhttp.core.RawHttpResponse; + +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; + +/** + * Implementation of {@link EmbeddedApplication} for POSIX serverless environments. + * + * @author Sahoo. + */ +@Singleton +public class ServerlessApplication implements EmbeddedApplication { + + private final ApplicationContext applicationContext; + private final ApplicationConfiguration applicationConfiguration; + + /** + * Default constructor. + * + * @param applicationContext The application context + * @param applicationConfiguration The application configuration + */ + public ServerlessApplication(ApplicationContext applicationContext, + ApplicationConfiguration applicationConfiguration) { + // TODO: Accept InputStream and OutputStream so that they can be configured using beans. + // default them to streams based on System.inheritedChannel if possible, else System.in/out. + 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. + } + + @Override + public @NonNull ServerlessApplication start() { + final ConversionService conversionService = new DefaultMutableConversionService(); + final ServletHttpHandler> servletHttpHandler = + new ServletHttpHandler<>(applicationContext, null) { + @Override + protected ServletExchange> createExchange(RawHttpRequest request, + RawHttpResponse response) { + throw new UnsupportedOperationException("Not expected in serverless mode."); + } + }; + try { + Channel channel = System.inheritedChannel(); + if (channel != null) { + try (InputStream in = Channels.newInputStream((ReadableByteChannel) channel); + OutputStream out = Channels.newOutputStream((WritableByteChannel) channel)) { + runIndefinitely(servletHttpHandler, conversionService, in, out); + } + } else { + runIndefinitely(servletHttpHandler, conversionService, System.in, System.out); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + void runIndefinitely(ServletHttpHandler> servletHttpHandler, + ConversionService conversionService, + InputStream in, + OutputStream out) throws IOException { + while (true) { + handleSingleRequest(servletHttpHandler, conversionService, in, out); + } + } + + void handleSingleRequest(ServletHttpHandler> servletHttpHandler, + ConversionService conversionService, + InputStream in, + OutputStream out) throws IOException { + ServletExchange> servletExchange = + new ServletExchange<>() { + private final ServletHttpRequest httpRequest = + new RawHttpBasedServletHttpRequest(in, conversionService); + + private final ServletHttpResponse, String> httpResponse = + new RawHttpBasedServletHttpResponse(conversionService); + + @Override + public ServletHttpRequest getRequest() { + return httpRequest; + } + + @Override + public ServletHttpResponse, String> getResponse() { + return httpResponse; + } + }; + + servletHttpHandler.service(servletExchange); + RawHttpResponse rawHttpResponse = servletExchange.getResponse().getNativeResponse(); + rawHttpResponse.writeTo(out); + } + +} diff --git a/http-poja/src/test/resources/logback.xml b/http-poja/src/test/resources/logback.xml new file mode 100644 index 000000000..ef2b2f918 --- /dev/null +++ b/http-poja/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + System.err + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/settings.gradle b/settings.gradle index d4ba2390f..41ac523ae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,6 +31,7 @@ include 'servlet-engine' include 'http-server-jetty' include 'http-server-undertow' include 'http-server-tomcat' +include 'http-poja' include 'test-suite-http-server-tck-tomcat' include 'test-suite-http-server-tck-undertow' include 'test-suite-http-server-tck-jetty' From 55c1c99a37392f400e031dc8877cd9e7aeea0882 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Sun, 16 Jun 2024 21:09:39 -0400 Subject: [PATCH 084/180] GCN-4622 Create a simple test for the serverless application The test uses pipes to communicate with the server instead of STDIN and STDOUT and currently sends requests and receives responses as text, which can be improved. --- http-poja/build.gradle | 1 + .../http/poja/ServerlessApplication.java | 28 +++- .../poja/BaseServerlessApplicationSpec.groovy | 132 ++++++++++++++++++ .../http/poja/SimpleServerSpec.groovy | 132 ++++++++++++++++++ 4 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy create mode 100644 http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy diff --git a/http-poja/build.gradle b/http-poja/build.gradle index 1e29fd0b0..c0a43af46 100644 --- a/http-poja/build.gradle +++ b/http-poja/build.gradle @@ -7,5 +7,6 @@ dependencies { implementation("com.athaydes.rawhttp:rawhttp-core:2.4.1") implementation("com.athaydes.rawhttp:rawhttp-cookies:0.2.1") + testImplementation(mnSerde.micronaut.serde.jackson) } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java index 5f4587358..3e3dc6999 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -61,8 +61,14 @@ public boolean isRunning() { return true; // once this bean is instantiated, we assume it's running, so return true. } - @Override - public @NonNull ServerlessApplication start() { + /** + * Run the application using a particular channel + * + * @param input The input stream + * @param output The output stream + * @return The application + */ + protected @NonNull ServerlessApplication start(InputStream input, OutputStream output) { final ConversionService conversionService = new DefaultMutableConversionService(); final ServletHttpHandler> servletHttpHandler = new ServletHttpHandler<>(applicationContext, null) { @@ -72,20 +78,30 @@ protected ServletExchange> createExchange( throw new UnsupportedOperationException("Not expected in serverless mode."); } }; + try { + runIndefinitely(servletHttpHandler, conversionService, input, output); + + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + @Override + public @NonNull ServerlessApplication start() { try { Channel channel = System.inheritedChannel(); if (channel != null) { try (InputStream in = Channels.newInputStream((ReadableByteChannel) channel); OutputStream out = Channels.newOutputStream((WritableByteChannel) channel)) { - runIndefinitely(servletHttpHandler, conversionService, in, out); + return start(in, out); } } else { - runIndefinitely(servletHttpHandler, conversionService, System.in, System.out); + return start(System.in, System.out); } } catch (IOException e) { - throw new RuntimeException(e); + throw new RuntimeException(); } - return this; } void runIndefinitely(ServletHttpHandler> servletHttpHandler, diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy new file mode 100644 index 000000000..00a15d131 --- /dev/null +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -0,0 +1,132 @@ +package io.micronaut.http.poja + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Replaces +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.ApplicationConfiguration +import io.micronaut.session.Session +import io.micronaut.session.SessionStore +import io.micronaut.session.http.HttpSessionFilter +import io.micronaut.session.http.HttpSessionIdEncoder +import io.micronaut.session.http.HttpSessionIdResolver +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.reactivestreams.Publisher +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 ServerlessApplication} that creates 2 + * pipes to communicate with the server and simplifies reading and writing to them. + */ + @Singleton + @Replaces(ServerlessApplication.class) + static class TestingServerlessApplication extends ServerlessApplication { + + 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 + ServerlessApplication 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', '') + } + } + + /** + * Not sure why this is required + * TODO fix this + * + * @author Andriy + */ + @Filter("**/*") + @Replaces(HttpSessionFilter) + static class DisabledSessionFilter extends HttpSessionFilter { + + DisabledSessionFilter(SessionStore sessionStore, HttpSessionIdResolver[] resolvers, HttpSessionIdEncoder[] encoders) { + super(sessionStore, resolvers, encoders) + } + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return chain.proceed(request) + } + } + +} diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy new file mode 100644 index 000000000..a29364c32 --- /dev/null +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy @@ -0,0 +1,132 @@ +package io.micronaut.http.poja + + +import io.micronaut.context.annotation.Property +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 +@Property(name = "micronaut.security.enabled", value = "false") +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-Type: text/plain + Content-Length: 32 + + 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-Type: application/json + Content-Length: 148 + + {"_links":{"self":[{"href":"http://h/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".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-Type: text/plain + Content-Length: 13 + + 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-Type: text/plain + Content-Length: 15 + + 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" + } + + } + +} From f74f8147f7465047aaf9ef01fcec9099a9827700 Mon Sep 17 00:00:00 2001 From: Sanjeeb Sahoo Date: Mon, 17 Jun 2024 14:47:52 -0700 Subject: [PATCH 085/180] added copyright and license. --- http-poja/build.gradle | 15 +++++++++++++++ .../http/poja/RawHttpBasedServletHttpRequest.java | 15 +++++++++++++++ .../poja/RawHttpBasedServletHttpResponse.java | 15 +++++++++++++++ .../http/poja/ServerlessApplication.java | 15 +++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/http-poja/build.gradle b/http-poja/build.gradle index c0a43af46..ef026b7f4 100644 --- a/http-poja/build.gradle +++ b/http-poja/build.gradle @@ -1,3 +1,18 @@ +/* + * 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.implementation") } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java index 5ed301bbe..0e3d47c75 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java @@ -1,3 +1,18 @@ +/* + * 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. + */ package io.micronaut.http.poja; import io.micronaut.core.annotation.NonNull; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java index d8ba236ff..5fc01ffe6 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java @@ -1,3 +1,18 @@ +/* + * 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. + */ package io.micronaut.http.poja; import io.micronaut.core.annotation.NonNull; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java index 3e3dc6999..290d5ade0 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -1,3 +1,18 @@ +/* + * 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. + */ package io.micronaut.http.poja; import io.micronaut.context.ApplicationContext; From 6cdb40e246bf033229326327fa851c122011cfdb Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 18 Jun 2024 14:42:17 -0400 Subject: [PATCH 086/180] GCN-4624 Add the Micronaut server TCK for POJA --- settings.gradle | 1 + test-suite-http-server-tck-poja/build.gradle | 28 +++ .../test/BaseServerlessApplicationSpec.groovy | 46 +++++ .../http/poja/test/SimpleServerSpec.groovy | 116 +++++++++++ .../server/tck/poja/PojaServerTestSuite.java | 66 ++++++ .../server/tck/poja/PojaServerUnderTest.java | 99 +++++++++ .../tck/poja/PojaServerUnderTestProvider.java | 30 +++ .../adapter/TestingServerlessApplication.java | 190 ++++++++++++++++++ ...micronaut.http.tck.ServerUnderTestProvider | 1 + 9 files changed, 577 insertions(+) create mode 100644 test-suite-http-server-tck-poja/build.gradle create mode 100644 test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy create mode 100644 test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy create mode 100644 test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java create mode 100644 test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java create mode 100644 test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java create mode 100644 test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java create mode 100644 test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider diff --git a/settings.gradle b/settings.gradle index 41ac523ae..4489cafd8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -35,4 +35,5 @@ include 'http-poja' 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' include 'test-suite-kotlin-jetty' diff --git a/test-suite-http-server-tck-poja/build.gradle b/test-suite-http-server-tck-poja/build.gradle new file mode 100644 index 000000000..a70fc4895 --- /dev/null +++ b/test-suite-http-server-tck-poja/build.gradle @@ -0,0 +1,28 @@ +plugins { + id("io.micronaut.build.internal.servlet.implementation") + id("java-library") +} + +dependencies { + testAnnotationProcessor(platform(mn.micronaut.core.bom)) + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(platform(mn.micronaut.core.bom)) + testImplementation(mn.micronaut.inject.java) + + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.http.server.tck) + testImplementation(libs.junit.platform.engine) + testImplementation(mn.micronaut.jackson.databind) + testImplementation(libs.junit.jupiter.engine) + testRuntimeOnly(mnLogging.logback.classic) + + testRuntimeOnly(mnValidation.micronaut.validation) + + testImplementation(projects.micronautHttpPoja) + testImplementation(mnSerde.micronaut.serde.jackson) + testImplementation(mn.micronaut.http.client) + + testImplementation("com.athaydes.rawhttp:rawhttp-core:2.4.1") + testImplementation("com.athaydes.rawhttp:rawhttp-cookies:0.2.1") +} + diff --git a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy new file mode 100644 index 000000000..185ca64ff --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy @@ -0,0 +1,46 @@ +package io.micronaut.http.poja.test + +import io.micronaut.context.annotation.Replaces +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.http.server.tck.poja.adapter.TestingServerlessApplication +import io.micronaut.session.Session +import io.micronaut.session.SessionStore +import io.micronaut.session.http.HttpSessionFilter +import io.micronaut.session.http.HttpSessionIdEncoder +import io.micronaut.session.http.HttpSessionIdResolver +import jakarta.inject.Inject +import org.reactivestreams.Publisher +import spock.lang.Specification + +/** + * A base class for serverless application test + */ +abstract class BaseServerlessApplicationSpec extends Specification { + + @Inject + TestingServerlessApplication app + + /** + * Not sure why this is required + * TODO fix this + * + * @author Andriy Dmytruk + */ + @Filter("**/*") + @Replaces(HttpSessionFilter) + static class DisabledSessionFilter extends HttpSessionFilter { + + DisabledSessionFilter(SessionStore sessionStore, HttpSessionIdResolver[] resolvers, HttpSessionIdEncoder[] encoders) { + super(sessionStore, resolvers, encoders) + } + + @Override + Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return chain.proceed(request) + } + } + +} diff --git a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy new file mode 100644 index 000000000..b2e7803fb --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy @@ -0,0 +1,116 @@ +package io.micronaut.http.poja.test + + +import io.micronaut.context.annotation.Property +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.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.extensions.spock.annotation.MicronautTest + +@MicronautTest +@Property(name = "micronaut.security.enabled", value = "false") +class SimpleServerSpec extends BaseServerlessApplicationSpec { + + + void "test GET method"() { + given: + BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() + + when: + HttpResponse response = client.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"() { + given: + BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() + + when: + HttpResponse response = client.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"() { + given: + BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() + + when: + HttpResponse response = client.exchange(HttpRequest.DELETE("/test").header("Host", "h")) + + then: + response.status() == HttpStatus.OK + response.getBody(String.class).isEmpty() + } + + void "test POST method"() { + given: + BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() + + when: + HttpResponse response = client.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 PUT method"() { + given: + BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() + + when: + HttpResponse response = client.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" + } + + @Put("/{name}") + @Status(HttpStatus.OK) + String update(@NonNull String name) { + return "Hello, " + name + "!\n" + } + + } + +} diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java new file mode 100644 index 000000000..32370f6fc --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -0,0 +1,66 @@ +/* + * 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({ + // 78 tests of 158 fail + "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", + "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", + "io.micronaut.http.server.tck.tests.VersionTest", + "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", + "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", + "io.micronaut.http.server.tck.tests.OctetTest", + "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", + "io.micronaut.http.server.tck.tests.forms.FormsInputNumberOptionalTest", + "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", + "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", + "io.micronaut.http.server.tck.tests.forms.FormsSubmissionsWithListsTest", + "io.micronaut.http.server.tck.tests.ErrorHandlerTest", + "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", + "io.micronaut.http.server.tck.tests.MiscTest", + "io.micronaut.http.server.tck.tests.BodyTest", + "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", + "io.micronaut.http.server.tck.tests.CookiesTest", + "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", + "io.micronaut.http.server.tck.tests.RemoteAddressTest", + "io.micronaut.http.server.tck.tests.binding.LocalDateTimeTest", + "io.micronaut.http.server.tck.tests.cors.CrossOriginTest", + "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", + "io.micronaut.http.server.tck.tests.FiltersTest", + "io.micronaut.http.server.tck.tests.MissingBodyAnnotationTest", + "io.micronaut.http.server.tck.tests.FilterProxyTest", + "io.micronaut.http.server.tck.tests.HeadersTest", + "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", + "io.micronaut.http.server.tck.tests.endpoints.health.HealthTest", + "io.micronaut.http.server.tck.tests.bodywritable.HtmlBodyWritableTest", + "io.micronaut.http.server.tck.tests.filter.HttpServerFilterTest", + "io.micronaut.http.server.tck.tests.BodyArgumentTest", + "io.micronaut.http.server.tck.tests.ResponseStatusTest", + "io.micronaut.http.server.tck.tests.FluxTest", + "io.micronaut.http.server.tck.tests.ConsumesTest" +}) +public class PojaServerTestSuite { +} diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java new file mode 100644 index 000000000..800f8859f --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java @@ -0,0 +1,99 @@ +/* + * 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.server.tck.poja.adapter.TestingServerlessApplication; +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; + +public class PojaServerUnderTest implements ServerUnderTest { + + private static final Logger LOG = LoggerFactory.getLogger(PojaServerUnderTest.class); + + private final ApplicationContext applicationContext; + private final TestingServerlessApplication application; + private final BlockingHttpClient client; + private final int port; + + public PojaServerUnderTest(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(TestingServerlessApplication.class) + .orElseThrow(() -> new IllegalStateException("TestingServerlessApplication bean is required")); + application.start(); + port = application.getPort(); + try { + client = HttpClient.create(new URL("http://localhost:" + port)) + .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(); + application.close(); + } +} diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java new file mode 100644 index 000000000..74fb84e10 --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java @@ -0,0 +1,30 @@ +/* + * 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 PojaServerUnderTestProvider implements ServerUnderTestProvider { + + @Override + public ServerUnderTest getServer(Map properties) { + return new PojaServerUnderTest(properties); + } + +} diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java new file mode 100644 index 000000000..9fe0a179d --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java @@ -0,0 +1,190 @@ +package io.micronaut.http.server.tck.poja.adapter; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.poja.ServerlessApplication; +import io.micronaut.runtime.ApplicationConfiguration; +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.ServerSocket; +import java.net.Socket; +import java.nio.CharBuffer; +import java.nio.channels.Channels; +import java.nio.channels.Pipe; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * An extension of {@link io.micronaut.http.poja.ServerlessApplication} that creates 2 + * pipes to communicate with the server and simplifies reading and writing to them. + * + * @author Andriy Dmytruk + */ +@Singleton +@Replaces(ServerlessApplication.class) +public class TestingServerlessApplication extends ServerlessApplication { + + private int port; + private ServerSocket serverSocket; + private OutputStream serverInput; + private InputStream serverOutput; + + /** + * Default constructor. + * + * @param applicationContext The application context + * @param applicationConfiguration The application configuration + */ + public TestingServerlessApplication(ApplicationContext applicationContext, ApplicationConfiguration applicationConfiguration) { + super(applicationContext, applicationConfiguration); + } + + private void createServerSocket() { + IOException exception = null; + for (int i = 0; i < 100; ++i) { + port = new 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 TestingServerlessApplication start() { + createServerSocket(); + + Pipe inputPipe, outputPipe; + 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 + new Thread(() -> + start( + Channels.newInputStream(inputPipe.source()), + Channels.newOutputStream(outputPipe.sink()) + ) + ).start(); + + // Run the thread that sends requests to the server + new Thread(() -> { + while (true) { + try { + Socket socket = serverSocket.accept(); + String request = readInputStream(socket.getInputStream()); + serverInput.write(request.getBytes()); + serverInput.write(new byte[]{'\n'}); + + String response = readInputStream(serverOutput); + socket.getOutputStream().write(response.getBytes()); + socket.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }).start(); + + return this; + } + + @Override + public @NonNull ServerlessApplication stop() { + try { + serverSocket.close(); + } catch (IOException ignored) { + } + return super.stop(); + } + + String readInputStream(InputStream inputStream) { + BufferedReader input = new BufferedReader(new InputStreamReader(inputStream)); + 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) { + result.append("\n"); + if (body) { + currentSize += 1; + } + lastLine = line; + } else { + lastLine = lastLine + line; + } + if (body) { + currentSize += line.length(); + } + result.append(line); + if (i < lines.size() - 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().replace("\r", ""); + } + + 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; + } + + public int getPort() { + return port; + } + +} diff --git a/test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider new file mode 100644 index 000000000..960de8c73 --- /dev/null +++ b/test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.server.tck.poja.PojaServerUnderTestProvider From ccb2e5e85f390ea49bb24c07cb54bc7b8eecf7b3 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 24 Jun 2024 14:51:59 -0400 Subject: [PATCH 087/180] Add body support to the poja - Add support for getting the body from the RawHttpRequest - Add support for reading cookies from the request --- .../poja/RawHttpBasedServletHttpRequest.java | 245 ++++++++++- .../http/poja/ServerlessApplication.java | 20 +- .../poja/fork/netty/QueryStringDecoder.java | 411 ++++++++++++++++++ .../server/tck/poja/PojaServerTestSuite.java | 3 +- 4 files changed, 661 insertions(+), 18 deletions(-) create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java index 0e3d47c75..bd8e4ebcb 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java @@ -19,21 +19,42 @@ import io.micronaut.core.annotation.Nullable; 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.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpParameters; +import io.micronaut.http.MediaType; +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.cookie.Cookie; import io.micronaut.http.cookie.Cookies; +import io.micronaut.http.poja.fork.netty.QueryStringDecoder; +import io.micronaut.http.simple.cookies.SimpleCookies; import io.micronaut.servlet.http.ServletHttpRequest; +import io.micronaut.servlet.http.body.InputStreamByteBody; import rawhttp.cookies.ServerCookieHelper; import rawhttp.core.RawHttp; import rawhttp.core.RawHttpHeaders; import rawhttp.core.RawHttpRequest; +import rawhttp.core.body.BodyReader; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpCookie; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -41,35 +62,49 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Function; import java.util.stream.Collectors; /** * @author Sahoo. */ -class RawHttpBasedServletHttpRequest implements ServletHttpRequest { +class RawHttpBasedServletHttpRequest implements ServletHttpRequest, ServerHttpRequest { private final RawHttp rawHttp; - private final InputStream in; private final RawHttpRequest rawHttpRequest; + private final ByteBody byteBody; private final RawHttpBasedHeaders headers; + private final ConversionService conversionService; + private final MediaTypeCodecRegistry codecRegistry; private final RawHttpBasedParameters queryParameters; - public RawHttpBasedServletHttpRequest(InputStream in, ConversionService conversionService) { + public RawHttpBasedServletHttpRequest( + InputStream in, ConversionService conversionService, MediaTypeCodecRegistry codecRegistry, Executor ioExecutor + ) { this.rawHttp = new RawHttp(); - this.in = in; try { rawHttpRequest = rawHttp.parseRequest(in); -// System.err.println("DEBUG: Parsed following request from input stream: \n" + rawHttpRequest); - } catch (IOException e) { throw new RuntimeException(e); } headers = new RawHttpBasedHeaders(rawHttpRequest.getHeaders(), conversionService); + OptionalLong contentLength = rawHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_LENGTH) + .map(Long::parseLong).map(OptionalLong::of).orElse(OptionalLong.empty()); + + this.byteBody = rawHttpRequest.getBody() + .map(b -> InputStreamByteBody.create(b.asRawStream(), contentLength, ioExecutor)) + .orElse(InputStreamByteBody.create(new ByteArrayInputStream(new byte[0]), OptionalLong.of(0), ioExecutor)); + this.conversionService = conversionService; + this.codecRegistry = codecRegistry; queryParameters = new RawHttpBasedParameters(getUri().getRawQuery(), conversionService); } @@ -92,9 +127,13 @@ public RawHttpRequest getNativeRequest() { @Override public @NonNull Cookies getCookies() { -// var cookies = ServerCookieHelper.readClientCookies(rawHttpRequest); - // TODO - throw new UnsupportedOperationException("TBD"); + Map cookiesMap = ServerCookieHelper.readClientCookies(rawHttpRequest) + .stream() + .map(RawHttpCookie::new) + .collect(Collectors.toMap(Cookie::getName, Function.identity())); + SimpleCookies cookies = new SimpleCookies(conversionService); + cookies.putAll(cookiesMap); + return cookies; } @Override @@ -125,13 +164,199 @@ public RawHttpRequest getNativeRequest() { } @Override - public @NonNull Optional getBody() { + public @NonNull Optional getBody(@NonNull ArgumentConversionContext conversionContext) { + Optional reader = rawHttpRequest.getBody(); + if (reader.isEmpty()) { + return Optional.empty(); + } + reader.get().asRawStream(); + + 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()) { + try (CloseableByteBody body = byteBody().split(SplitBackpressureMode.FASTEST)) { + String content = IOUtils.readText(new BufferedReader(new InputStreamReader( + body.toInputStream(), getCharacterEncoding() + ))); + ConvertibleMultiValues form = parseFormData(content); + if (ConvertibleValues.class == type || Object.class == type) { + return Optional.of((T) form); + } else { + return conversionService.convert(form.asMap(), arg); + } + } catch (IOException e) { + throw new RuntimeException("Unable to parse body", e); + } + } + + 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); + } + } + + 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); + } + + public boolean isFormSubmission() { + MediaType contentType = getContentType().orElse(null); + return MediaType.APPLICATION_FORM_URLENCODED_TYPE.equals(contentType) + || MediaType.MULTIPART_FORM_DATA_TYPE.equals(contentType); + } + + /** + * A method that allows consuming body. + * + * @return The result + * @param The function return value + */ + public T consumeBody(Function consumer) { + return consumer.apply(byteBody.split(SplitBackpressureMode.FASTEST).toInputStream()); + } + + @Override + public @NonNull Optional getBody() { // TODO: figure out what needs to be done. System.err.println("TBD: getBody() Retuning null body for now."); Thread.dumpStack(); return Optional.empty(); } + @Override + public @NonNull ByteBody byteBody() { + return byteBody; + } + + public record RawHttpCookie( + HttpCookie cookie + ) implements Cookie { + + @Override + public @NonNull String getName() { + return cookie.getName(); + } + + @Override + public @NonNull String getValue() { + return cookie.getValue(); + } + + @Override + public @Nullable String getDomain() { + return cookie.getDomain(); + } + + @Override + public @Nullable String getPath() { + return cookie.getPath(); + } + + @Override + public boolean isHttpOnly() { + return cookie.isHttpOnly(); + } + + @Override + public boolean isSecure() { + return cookie.getSecure(); + } + + @Override + public long getMaxAge() { + return cookie.getMaxAge(); + } + + @Override + public @NonNull Cookie maxAge(long maxAge) { + cookie.setMaxAge(maxAge); + return this; + } + + @Override + public @NonNull Cookie value(@NonNull String value) { + cookie.setValue(value); + return this; + } + + @Override + public @NonNull Cookie domain(@Nullable String domain) { + cookie.setDomain(domain); + return this; + } + + @Override + public @NonNull Cookie path(@Nullable String path) { + cookie.setPath(path); + return this; + } + + @Override + public @NonNull Cookie secure(boolean secure) { + cookie.setSecure(secure); + return this; + } + + @Override + public @NonNull Cookie httpOnly(boolean httpOnly) { + cookie.setHttpOnly(httpOnly); + return this; + } + + @Override + public int compareTo(Cookie o) { + int v = getName().compareTo(o.getName()); + if (v != 0) { + return v; + } + + v = compareNullableValue(getPath(), o.getPath()); + if (v != 0) { + return v; + } + + return compareNullableValue(getDomain(), o.getDomain()); + } + + private static int compareNullableValue(String first, String second) { + if (first == null) { + if (second != null) { + return -1; + } else { + return 0; + } + } else if (second == null) { + return 1; + } else { + return first.compareToIgnoreCase(second); + } + } + } + public static class RawHttpBasedHeaders implements HttpHeaders { private final RawHttpHeaders rawHttpHeaders; private final ConversionService conversionService; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java index 290d5ade0..c0e1b43ff 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -19,8 +19,11 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.DefaultMutableConversionService; +import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.scheduling.TaskExecutors; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpHandler; import io.micronaut.servlet.http.ServletHttpRequest; @@ -36,6 +39,8 @@ import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; /** * Implementation of {@link EmbeddedApplication} for POSIX serverless environments. @@ -84,7 +89,6 @@ public boolean isRunning() { * @return The application */ protected @NonNull ServerlessApplication start(InputStream input, OutputStream output) { - final ConversionService conversionService = new DefaultMutableConversionService(); final ServletHttpHandler> servletHttpHandler = new ServletHttpHandler<>(applicationContext, null) { @Override @@ -94,7 +98,7 @@ protected ServletExchange> createExchange( } }; try { - runIndefinitely(servletHttpHandler, conversionService, input, output); + runIndefinitely(servletHttpHandler, applicationContext, input, output); } catch (IOException e) { throw new RuntimeException(e); @@ -120,22 +124,26 @@ protected ServletExchange> createExchange( } void runIndefinitely(ServletHttpHandler> servletHttpHandler, - ConversionService conversionService, + ApplicationContext applicationContext, InputStream in, OutputStream out) throws IOException { while (true) { - handleSingleRequest(servletHttpHandler, conversionService, in, out); + handleSingleRequest(servletHttpHandler, applicationContext, in, out); } } void handleSingleRequest(ServletHttpHandler> servletHttpHandler, - ConversionService conversionService, + ApplicationContext applicationContext, InputStream in, OutputStream out) throws IOException { + ConversionService conversionService = applicationContext.getConversionService(); + MediaTypeCodecRegistry codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); + ExecutorService ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.IO)); + ServletExchange> servletExchange = new ServletExchange<>() { private final ServletHttpRequest httpRequest = - new RawHttpBasedServletHttpRequest(in, conversionService); + new RawHttpBasedServletHttpRequest(in, conversionService, codecRegistry, ioExecutor); private final ServletHttpResponse, String> httpResponse = new RawHttpBasedServletHttpResponse(conversionService); diff --git a/http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java b/http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java new file mode 100644 index 000000000..9a524139a --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java @@ -0,0 +1,411 @@ +/* + * 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.fork.netty; + +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. + */ +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/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index 32370f6fc..1e798d44b 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,7 +26,7 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 78 tests of 158 fail + // 89 tests of 188 fail "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", "io.micronaut.http.server.tck.tests.VersionTest", @@ -43,7 +43,6 @@ "io.micronaut.http.server.tck.tests.MiscTest", "io.micronaut.http.server.tck.tests.BodyTest", "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", - "io.micronaut.http.server.tck.tests.CookiesTest", "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", "io.micronaut.http.server.tck.tests.RemoteAddressTest", "io.micronaut.http.server.tck.tests.binding.LocalDateTimeTest", From 9ed8e986bb363577725f586a0e5a4787b25d718d Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 25 Jun 2024 10:55:21 -0400 Subject: [PATCH 088/180] Improve body support - Add a LimitingInputStream to improve body support. The stream does not allow reading after content length has been read. - Refactor into common base classes and specific RawHttp implementation that could be replaced. --- http-poja/build.gradle | 7 +- .../http/poja/PojaBinderRegistry.java | 55 ++++ .../micronaut/http/poja/PojaBodyBinder.java | 245 ++++++++++++++++ .../micronaut/http/poja/PojaHttpRequest.java | 267 ++++++++++++++++++ .../micronaut/http/poja/PojaHttpResponse.java | 13 + .../http/poja/ServerlessApplication.java | 6 +- .../RawHttpBasedServletHttpRequest.java | 132 +-------- .../RawHttpBasedServletHttpResponse.java | 6 +- test-suite-http-server-tck-poja/build.gradle | 2 +- .../test/BaseServerlessApplicationSpec.groovy | 33 +-- .../http/poja/test/SimpleServerSpec.groovy | 17 ++ .../server/tck/poja/PojaServerTestSuite.java | 50 ++-- 12 files changed, 646 insertions(+), 187 deletions(-) create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java rename http-poja/src/main/java/io/micronaut/http/poja/{ => rawhttp}/RawHttpBasedServletHttpRequest.java (68%) rename http-poja/src/main/java/io/micronaut/http/poja/{ => rawhttp}/RawHttpBasedServletHttpResponse.java (95%) diff --git a/http-poja/build.gradle b/http-poja/build.gradle index ef026b7f4..bd721822e 100644 --- a/http-poja/build.gradle +++ b/http-poja/build.gradle @@ -14,14 +14,17 @@ * limitations under the License. */ plugins { - id("io.micronaut.build.internal.servlet.implementation") + id("io.micronaut.build.internal.servlet.module") } dependencies { - api project(":micronaut-servlet-core") + api(projects.micronautServletCore) implementation("com.athaydes.rawhttp:rawhttp-core:2.4.1") implementation("com.athaydes.rawhttp:rawhttp-cookies:0.2.1") + compileOnly(mn.reactor) + compileOnly(mn.micronaut.json.core) + testImplementation(mnSerde.micronaut.serde.jackson) } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java new file mode 100644 index 000000000..077b3a3ec --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java @@ -0,0 +1,55 @@ +/* + * 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/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java new file mode 100644 index 000000000..78d147e70 --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java @@ -0,0 +1,245 @@ +/* + * 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 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) Optional.of(content); + } catch (IOException e) { + LOG.debug("Error occurred reading function body: {}", e.getMessage(), e); + return new ConversionFailedBindingResult<>(e); + } + }); + } 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) { + 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); + } + }); + } + + } + } + LOG.trace("Not a function request, falling back to default body decoding"); + return defaultBodyBinder.bind(context, source); + } + + 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 object 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 object 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/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java new file mode 100644 index 000000000..774fb0338 --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -0,0 +1,267 @@ +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.io.IOUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.MediaType; +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.fork.netty.QueryStringDecoder; +import io.micronaut.servlet.http.ServletHttpRequest; +import rawhttp.core.RawHttpRequest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +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 + * @author Andriy + */ +public abstract class PojaHttpRequest implements ServletHttpRequest, ServerHttpRequest { + + public static final Argument CONVERTIBLE_VALUES_ARGUMENT = Argument.of(ConvertibleValues.class); + + protected final ConversionService conversionService; + protected final MediaTypeCodecRegistry codecRegistry; + + public PojaHttpRequest( + ConversionService conversionService, + MediaTypeCodecRegistry codecRegistry + ) { + this.conversionService = conversionService; + this.codecRegistry = codecRegistry; + } + + @Override + public abstract CloseableByteBody byteBody(); + + /** + * A utility method that allows consuming body. + * + * @return The result + * @param The function return value + */ + public T consumeBody(Function consumer) { + try (CloseableByteBody byteBody = byteBody()) { + InputStream stream = new LimitingInputStream( + byteBody.toInputStream(), + byteBody.expectedLength().orElse(0) + ); + return consumer.apply(stream); + } + } + + @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()) { + return consumeBody(inputStream -> { + try { + String content = IOUtils.readText(new BufferedReader(new InputStreamReader( + inputStream, getCharacterEncoding() + ))); + ConvertibleMultiValues form = parseFormData(content); + if (ConvertibleValues.class == type || Object.class == type) { + return Optional.of((T) form); + } else { + return conversionService.convert(form.asMap(), arg); + } + } catch (IOException e) { + throw new RuntimeException("Unable to parse body", e); + } + }); + } + + 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); + } + } + + @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); + } + + 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); + } + + /** + * A wrapper around input stream that limits the maximum size to be read. + */ + public static class LimitingInputStream extends InputStream { + + private long size; + private final InputStream stream; + private final long maxSize; + + public LimitingInputStream(InputStream stream, long maxSize) { + this.maxSize = maxSize; + this.stream = stream; + } + + @Override + public synchronized void mark(int readlimit) { + stream.mark(readlimit); + } + + @Override + public int read() throws IOException { + return stream.read(); + } + + @Override + public int read(byte[] b) throws IOException { + synchronized(this) { + if (size >= maxSize) { + return -1; + } + int sizeRead = stream.read(b); + size += sizeRead; + return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + synchronized (this) { + if (size >= maxSize) { + return -1; + } + int sizeRead = stream.read(b, off, len); + size += sizeRead + off; + return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; + } + } + + @Override + public byte[] readAllBytes() throws IOException { + return stream.readAllBytes(); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + return stream.readNBytes(len); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return stream.readNBytes(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return stream.skip(n); + } + + @Override + public void skipNBytes(long n) throws IOException { + stream.skipNBytes(n); + } + + @Override + public int available() throws IOException { + return stream.available(); + } + + @Override + public void close() throws IOException { + stream.close(); + } + + @Override + public synchronized void reset() throws IOException { + stream.reset(); + } + + @Override + public boolean markSupported() { + return stream.markSupported(); + } + + @Override + public long transferTo(OutputStream out) throws IOException { + return stream.transferTo(out); + } + + @Override + public int hashCode() { + return stream.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return stream.equals(obj); + } + + @Override + public String toString() { + return stream.toString(); + } + } + +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java new file mode 100644 index 000000000..5e01a58d6 --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java @@ -0,0 +1,13 @@ +package io.micronaut.http.poja; + +import io.micronaut.servlet.http.ServletHttpResponse; +import rawhttp.core.RawHttpResponse; + +/** + * A base class for serverless POJA responses. + */ +public abstract class PojaHttpResponse implements ServletHttpResponse, String> { + + + +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java index c0e1b43ff..00956e8e0 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -18,8 +18,9 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.DefaultMutableConversionService; import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.http.poja.rawhttp.RawHttpBasedServletHttpRequest; +import io.micronaut.http.poja.rawhttp.RawHttpBasedServletHttpResponse; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.runtime.EmbeddedApplication; @@ -39,7 +40,6 @@ import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; /** @@ -138,7 +138,7 @@ void handleSingleRequest(ServletHttpHandler> servletExchange = new ServletExchange<>() { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java similarity index 68% rename from http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java rename to http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index bd8e4ebcb..c2a7cefc8 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -13,47 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja; +package io.micronaut.http.poja.rawhttp; 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.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.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpParameters; -import io.micronaut.http.MediaType; -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.cookie.Cookie; import io.micronaut.http.cookie.Cookies; -import io.micronaut.http.poja.fork.netty.QueryStringDecoder; +import io.micronaut.http.poja.PojaHttpRequest; import io.micronaut.http.simple.cookies.SimpleCookies; -import io.micronaut.servlet.http.ServletHttpRequest; import io.micronaut.servlet.http.body.InputStreamByteBody; import rawhttp.cookies.ServerCookieHelper; import rawhttp.core.RawHttp; import rawhttp.core.RawHttpHeaders; import rawhttp.core.RawHttpRequest; -import rawhttp.core.body.BodyReader; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.HttpCookie; import java.net.URI; import java.net.URLDecoder; @@ -62,34 +49,34 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; -import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; /** * @author Sahoo. */ -class RawHttpBasedServletHttpRequest implements ServletHttpRequest, ServerHttpRequest { +public class RawHttpBasedServletHttpRequest extends PojaHttpRequest { private final RawHttp rawHttp; private final RawHttpRequest rawHttpRequest; private final ByteBody byteBody; private final RawHttpBasedHeaders headers; - - private final ConversionService conversionService; - private final MediaTypeCodecRegistry codecRegistry; private final RawHttpBasedParameters queryParameters; public RawHttpBasedServletHttpRequest( - InputStream in, ConversionService conversionService, MediaTypeCodecRegistry codecRegistry, Executor ioExecutor + InputStream in, + ConversionService conversionService, + MediaTypeCodecRegistry codecRegistry, + ExecutorService ioExecutor ) { + super(conversionService, codecRegistry); this.rawHttp = new RawHttp(); try { rawHttpRequest = rawHttp.parseRequest(in); @@ -103,23 +90,9 @@ public RawHttpBasedServletHttpRequest( this.byteBody = rawHttpRequest.getBody() .map(b -> InputStreamByteBody.create(b.asRawStream(), contentLength, ioExecutor)) .orElse(InputStreamByteBody.create(new ByteArrayInputStream(new byte[0]), OptionalLong.of(0), ioExecutor)); - this.conversionService = conversionService; - this.codecRegistry = codecRegistry; queryParameters = new RawHttpBasedParameters(getUri().getRawQuery(), conversionService); } - @Override - public InputStream getInputStream() throws IOException { - return null; - } - - @Override - public BufferedReader getReader() throws IOException { - return null; -// return new BufferedReader(new InputStreamReader(getInputStream(), -// rawHttp.getOptions().getHttpHeadersOptions().getHeaderValuesCharset())); - } - @Override public RawHttpRequest getNativeRequest() { return rawHttpRequest; @@ -163,93 +136,14 @@ public RawHttpRequest getNativeRequest() { return new MutableConvertibleValuesMap<>(); } - @Override - public @NonNull Optional getBody(@NonNull ArgumentConversionContext conversionContext) { - Optional reader = rawHttpRequest.getBody(); - if (reader.isEmpty()) { - return Optional.empty(); - } - reader.get().asRawStream(); - - 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()) { - try (CloseableByteBody body = byteBody().split(SplitBackpressureMode.FASTEST)) { - String content = IOUtils.readText(new BufferedReader(new InputStreamReader( - body.toInputStream(), getCharacterEncoding() - ))); - ConvertibleMultiValues form = parseFormData(content); - if (ConvertibleValues.class == type || Object.class == type) { - return Optional.of((T) form); - } else { - return conversionService.convert(form.asMap(), arg); - } - } catch (IOException e) { - throw new RuntimeException("Unable to parse body", e); - } - } - - 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); - } - } - - 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); - } - - public boolean isFormSubmission() { - MediaType contentType = getContentType().orElse(null); - return MediaType.APPLICATION_FORM_URLENCODED_TYPE.equals(contentType) - || MediaType.MULTIPART_FORM_DATA_TYPE.equals(contentType); - } - - /** - * A method that allows consuming body. - * - * @return The result - * @param The function return value - */ - public T consumeBody(Function consumer) { - return consumer.apply(byteBody.split(SplitBackpressureMode.FASTEST).toInputStream()); - } - @Override public @NonNull Optional getBody() { - // TODO: figure out what needs to be done. - System.err.println("TBD: getBody() Retuning null body for now."); - Thread.dumpStack(); - return Optional.empty(); + return (Optional) getBody(Object.class); } @Override - public @NonNull ByteBody byteBody() { - return byteBody; + public @NonNull CloseableByteBody byteBody() { + return byteBody.split(SplitBackpressureMode.FASTEST); } public record RawHttpCookie( diff --git a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java similarity index 95% rename from http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java rename to http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index 5fc01ffe6..e6fb90318 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja; +package io.micronaut.http.poja.rawhttp; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -25,8 +25,8 @@ 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 io.micronaut.servlet.http.ServletHttpResponse; import rawhttp.core.HttpVersion; import rawhttp.core.RawHttpHeaders; import rawhttp.core.RawHttpResponse; @@ -43,7 +43,7 @@ /** * @author Sahoo. */ -class RawHttpBasedServletHttpResponse implements ServletHttpResponse, String> { +public class RawHttpBasedServletHttpResponse extends PojaHttpResponse { private final ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/test-suite-http-server-tck-poja/build.gradle b/test-suite-http-server-tck-poja/build.gradle index a70fc4895..b5308508d 100644 --- a/test-suite-http-server-tck-poja/build.gradle +++ b/test-suite-http-server-tck-poja/build.gradle @@ -1,5 +1,5 @@ plugins { - id("io.micronaut.build.internal.servlet.implementation") + id("io.micronaut.build.internal.servlet.module") id("java-library") } diff --git a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy index 185ca64ff..244cee057 100644 --- a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy +++ b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy @@ -1,20 +1,9 @@ package io.micronaut.http.poja.test -import io.micronaut.context.annotation.Replaces -import io.micronaut.http.HttpRequest -import io.micronaut.http.MutableHttpResponse -import io.micronaut.http.annotation.Filter -import io.micronaut.http.filter.ServerFilterChain + import io.micronaut.http.server.tck.poja.adapter.TestingServerlessApplication -import io.micronaut.session.Session -import io.micronaut.session.SessionStore -import io.micronaut.session.http.HttpSessionFilter -import io.micronaut.session.http.HttpSessionIdEncoder -import io.micronaut.session.http.HttpSessionIdResolver import jakarta.inject.Inject -import org.reactivestreams.Publisher import spock.lang.Specification - /** * A base class for serverless application test */ @@ -23,24 +12,4 @@ abstract class BaseServerlessApplicationSpec extends Specification { @Inject TestingServerlessApplication app - /** - * Not sure why this is required - * TODO fix this - * - * @author Andriy Dmytruk - */ - @Filter("**/*") - @Replaces(HttpSessionFilter) - static class DisabledSessionFilter extends HttpSessionFilter { - - DisabledSessionFilter(SessionStore sessionStore, HttpSessionIdResolver[] resolvers, HttpSessionIdEncoder[] encoders) { - super(sessionStore, resolvers, encoders) - } - - @Override - Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - return chain.proceed(request) - } - } - } diff --git a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy index b2e7803fb..edbb2c5ac 100644 --- a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy +++ b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy @@ -70,6 +70,18 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { response.getBody(String.class).get() == "Hello, Andriy\n" } + void "test POST method with unused body"() { + given: + BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() + + when: + HttpResponse response = client.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"() { given: BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() @@ -105,6 +117,11 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { return "Hello, " + name + "\n" } + @Post("/unused-body") + String createUnusedBody() { + return "Success!" + } + @Put("/{name}") @Status(HttpStatus.OK) String update(@NonNull String name) { diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index 1e798d44b..aea15569b 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,40 +26,36 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 89 tests of 188 fail - "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", - "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", - "io.micronaut.http.server.tck.tests.VersionTest", - "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", - "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", - "io.micronaut.http.server.tck.tests.OctetTest", - "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", - "io.micronaut.http.server.tck.tests.forms.FormsInputNumberOptionalTest", - "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", - "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", - "io.micronaut.http.server.tck.tests.forms.FormsSubmissionsWithListsTest", - "io.micronaut.http.server.tck.tests.ErrorHandlerTest", - "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", - "io.micronaut.http.server.tck.tests.MiscTest", - "io.micronaut.http.server.tck.tests.BodyTest", + // 74 tests of 188 fail + "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", + "io.micronaut.http.server.tck.tests.cors.CorsDisabledByDefaultTest", "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", - "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", - "io.micronaut.http.server.tck.tests.RemoteAddressTest", - "io.micronaut.http.server.tck.tests.binding.LocalDateTimeTest", "io.micronaut.http.server.tck.tests.cors.CrossOriginTest", - "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", - "io.micronaut.http.server.tck.tests.FiltersTest", - "io.micronaut.http.server.tck.tests.MissingBodyAnnotationTest", + "io.micronaut.http.server.tck.tests.ErrorHandlerTest", + "io.micronaut.http.server.tck.tests.ExpressionTest", + "io.micronaut.http.server.tck.tests.FilterErrorTest", "io.micronaut.http.server.tck.tests.FilterProxyTest", + "io.micronaut.http.server.tck.tests.FiltersTest", "io.micronaut.http.server.tck.tests.HeadersTest", - "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", + "io.micronaut.http.server.tck.tests.endpoints.health.HealthResultTest", "io.micronaut.http.server.tck.tests.endpoints.health.HealthTest", "io.micronaut.http.server.tck.tests.bodywritable.HtmlBodyWritableTest", "io.micronaut.http.server.tck.tests.filter.HttpServerFilterTest", - "io.micronaut.http.server.tck.tests.BodyArgumentTest", - "io.micronaut.http.server.tck.tests.ResponseStatusTest", - "io.micronaut.http.server.tck.tests.FluxTest", - "io.micronaut.http.server.tck.tests.ConsumesTest" + "io.micronaut.http.server.tck.tests.codec.JsonCodecAdditionalTypeTest", + "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", + "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", + "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", + "io.micronaut.http.server.tck.tests.MissingBodyAnnotationTest", + "io.micronaut.http.server.tck.tests.OctetTest", + "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", + "io.micronaut.http.server.tck.tests.PublisherExceptionHandlerTest", + "io.micronaut.http.server.tck.tests.RemoteAddressTest", + "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", + "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", + "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", + "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", + "io.micronaut.http.server.tck.tests.textplain.TxtPlainBigDecimalTest", + "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", }) public class PojaServerTestSuite { } From 9ee9de1cc2b28a1cae855c6ea4405bccaadda518 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 25 Jun 2024 14:20:29 -0400 Subject: [PATCH 089/180] Improvements with closing client and streams --- gradle/libs.versions.toml | 5 ++ http-poja/build.gradle | 4 +- .../micronaut/http/poja/PojaHttpRequest.java | 4 +- .../micronaut/http/poja/PojaHttpResponse.java | 2 +- .../http/poja/ServerlessApplication.java | 47 ++++++++++--------- .../RawHttpBasedServletHttpRequest.java | 9 ++-- .../RawHttpBasedServletHttpResponse.java | 27 ++++++----- .../server/tck/poja/PojaServerTestSuite.java | 11 +---- .../server/tck/poja/PojaServerUnderTest.java | 2 +- .../adapter/TestingServerlessApplication.java | 30 ++++++++---- 10 files changed, 79 insertions(+), 62 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb50092ba..d051f679b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,8 @@ micronaut-validation = "4.7.0" google-cloud-functions = '1.1.0' kotlin = "1.9.25" micronaut-logging = "1.3.0" +rawhttp-core = "2.4.1" +rawhttp-cookies = "0.2.1" # Micronaut micronaut-gradle-plugin = "4.4.2" @@ -55,6 +57,9 @@ jetty-alpn-conscrypt-server = { module = 'org.eclipse.jetty:jetty-alpn-conscrypt kotest-runner = { module = 'io.kotest:kotest-runner-junit5', version.ref = 'kotest-runner' } bcpkix = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix" } +rawhttp-core = { module = "com.athaydes.rawhttp:rawhttp-core", version.ref = "rawhttp-core" } +rawhttp-cookies = { module = "com.athaydes.rawhttp:rawhttp-cookies", version.ref = "rawhttp-cookies" } + google-cloud-functions = { module = 'com.google.cloud.functions:functions-framework-api', version.ref = 'google-cloud-functions' } # Gradle gradle-micronaut = { module = "io.micronaut.gradle:micronaut-gradle-plugin", version.ref = "micronaut-gradle-plugin" } diff --git a/http-poja/build.gradle b/http-poja/build.gradle index bd721822e..408777af9 100644 --- a/http-poja/build.gradle +++ b/http-poja/build.gradle @@ -19,8 +19,8 @@ plugins { dependencies { api(projects.micronautServletCore) - implementation("com.athaydes.rawhttp:rawhttp-core:2.4.1") - implementation("com.athaydes.rawhttp:rawhttp-cookies:0.2.1") + implementation(libs.rawhttp.core) + implementation(libs.rawhttp.cookies) compileOnly(mn.reactor) compileOnly(mn.micronaut.json.core) diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index 774fb0338..e1a2e348b 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -116,12 +116,12 @@ inputStream, getCharacterEncoding() @Override public InputStream getInputStream() { - return byteBody().split(SplitBackpressureMode.FASTEST).toInputStream(); + return byteBody().toInputStream(); } @Override public BufferedReader getReader() { - return new BufferedReader(new InputStreamReader(getInputStream())); + return new BufferedReader(new InputStreamReader(byteBody().toInputStream())); } /** diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java index 5e01a58d6..20c11f608 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java @@ -6,7 +6,7 @@ /** * A base class for serverless POJA responses. */ -public abstract class PojaHttpResponse implements ServletHttpResponse, String> { +public abstract class PojaHttpResponse implements ServletHttpResponse, T> { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java index 00956e8e0..aeca1d76d 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -27,8 +27,6 @@ import io.micronaut.scheduling.TaskExecutors; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpHandler; -import io.micronaut.servlet.http.ServletHttpRequest; -import io.micronaut.servlet.http.ServletHttpResponse; import jakarta.inject.Singleton; import rawhttp.core.RawHttpRequest; import rawhttp.core.RawHttpResponse; @@ -99,7 +97,6 @@ protected ServletExchange> createExchange( }; try { runIndefinitely(servletHttpHandler, applicationContext, input, output); - } catch (IOException e) { throw new RuntimeException(e); } @@ -140,28 +137,36 @@ void handleSingleRequest(ServletHttpHandler> servletExchange = - new ServletExchange<>() { - private final ServletHttpRequest httpRequest = - new RawHttpBasedServletHttpRequest(in, conversionService, codecRegistry, ioExecutor); + RawHttpExchange exchange = new RawHttpExchange( + new RawHttpBasedServletHttpRequest<>(in, conversionService, codecRegistry, ioExecutor), + new RawHttpBasedServletHttpResponse(conversionService) + ); - private final ServletHttpResponse, String> httpResponse = - new RawHttpBasedServletHttpResponse(conversionService); + servletHttpHandler.service(exchange); + RawHttpResponse rawHttpResponse = exchange.getResponse().getNativeResponse(); + rawHttpResponse.writeTo(out); + } - @Override - public ServletHttpRequest getRequest() { - return httpRequest; - } + @Override + public @NonNull ServerlessApplication stop() { + return EmbeddedApplication.super.stop(); + } - @Override - public ServletHttpResponse, String> getResponse() { - return httpResponse; - } - }; + public record RawHttpExchange( + RawHttpBasedServletHttpRequest httpRequest, + RawHttpBasedServletHttpResponse httpResponse + ) implements ServletExchange> { + + @Override + public RawHttpBasedServletHttpRequest getRequest() { + return httpRequest; + } + + @Override + public RawHttpBasedServletHttpResponse getResponse() { + return httpResponse; + } - servletHttpHandler.service(servletExchange); - RawHttpResponse rawHttpResponse = servletExchange.getResponse().getNativeResponse(); - rawHttpResponse.writeTo(out); } } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index c2a7cefc8..d46908117 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -37,6 +37,7 @@ import rawhttp.core.RawHttp; import rawhttp.core.RawHttpHeaders; import rawhttp.core.RawHttpRequest; +import rawhttp.core.body.BodyReader; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -56,7 +57,6 @@ import java.util.OptionalLong; import java.util.Set; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; @@ -87,9 +87,10 @@ public RawHttpBasedServletHttpRequest( OptionalLong contentLength = rawHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_LENGTH) .map(Long::parseLong).map(OptionalLong::of).orElse(OptionalLong.empty()); - this.byteBody = rawHttpRequest.getBody() - .map(b -> InputStreamByteBody.create(b.asRawStream(), contentLength, ioExecutor)) - .orElse(InputStreamByteBody.create(new ByteArrayInputStream(new byte[0]), OptionalLong.of(0), ioExecutor)); + InputStream stream = rawHttpRequest.getBody() + .map(BodyReader::asRawStream) + .orElse(new ByteArrayInputStream(new byte[0])); + this.byteBody = InputStreamByteBody.create(stream, contentLength, ioExecutor); queryParameters = new RawHttpBasedParameters(getUri().getRawQuery(), conversionService); } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index e6fb90318..ff92d8a2c 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -43,7 +43,7 @@ /** * @author Sahoo. */ -public class RawHttpBasedServletHttpResponse extends PojaHttpResponse { +public class RawHttpBasedServletHttpResponse extends PojaHttpResponse { private final ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -51,17 +51,18 @@ public class RawHttpBasedServletHttpResponse extends PojaHttpResponse { private String reason = HttpStatus.OK.getReason(); - private String body; private final SimpleHttpHeaders headers; private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); + private T bodyObject; + public RawHttpBasedServletHttpResponse(ConversionService conversionService) { this.headers = new SimpleHttpHeaders(conversionService); } @Override - public RawHttpResponse getNativeResponse() { + public RawHttpResponse getNativeResponse() { headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size())); return new RawHttpResponse<>(null, null, @@ -87,18 +88,24 @@ public BufferedWriter getWriter() throws IOException { } @Override - public MutableHttpResponse cookie(Cookie cookie) { + public MutableHttpResponse cookie(Cookie cookie) { return this; } @Override - public MutableHttpResponse body(@Nullable T body) { - // TODO - throw new UnsupportedOperationException("TBD"); + 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) { + public MutableHttpResponse status(int code, CharSequence message) { this.code = code; if (message == null) { this.reason = HttpStatus.getDefaultReason(code); @@ -128,8 +135,4 @@ public MutableHttpHeaders getHeaders() { return attributes; } - @Override - public @NonNull Optional getBody() { - return Optional.ofNullable(body); - } } diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index aea15569b..086335551 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,35 +26,26 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 74 tests of 188 fail + // 54 tests of 188 fail "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", - "io.micronaut.http.server.tck.tests.cors.CorsDisabledByDefaultTest", "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", "io.micronaut.http.server.tck.tests.cors.CrossOriginTest", "io.micronaut.http.server.tck.tests.ErrorHandlerTest", - "io.micronaut.http.server.tck.tests.ExpressionTest", - "io.micronaut.http.server.tck.tests.FilterErrorTest", "io.micronaut.http.server.tck.tests.FilterProxyTest", "io.micronaut.http.server.tck.tests.FiltersTest", - "io.micronaut.http.server.tck.tests.HeadersTest", - "io.micronaut.http.server.tck.tests.endpoints.health.HealthResultTest", "io.micronaut.http.server.tck.tests.endpoints.health.HealthTest", "io.micronaut.http.server.tck.tests.bodywritable.HtmlBodyWritableTest", "io.micronaut.http.server.tck.tests.filter.HttpServerFilterTest", - "io.micronaut.http.server.tck.tests.codec.JsonCodecAdditionalTypeTest", "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", - "io.micronaut.http.server.tck.tests.MissingBodyAnnotationTest", "io.micronaut.http.server.tck.tests.OctetTest", "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", - "io.micronaut.http.server.tck.tests.PublisherExceptionHandlerTest", "io.micronaut.http.server.tck.tests.RemoteAddressTest", "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", - "io.micronaut.http.server.tck.tests.textplain.TxtPlainBigDecimalTest", "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", }) public class PojaServerTestSuite { diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java index 800f8859f..544282fef 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java @@ -94,6 +94,6 @@ public Optional getPort() { @Override public void close() throws IOException { applicationContext.close(); - application.close(); + client.close(); } } diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java index 9fe0a179d..3472425b4 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java @@ -38,6 +38,10 @@ public class TestingServerlessApplication extends ServerlessApplication { private OutputStream serverInput; private InputStream serverOutput; + private Pipe inputPipe; + private Pipe outputPipe; + private Thread serverThread; + /** * Default constructor. * @@ -66,7 +70,6 @@ private void createServerSocket() { public TestingServerlessApplication start() { createServerSocket(); - Pipe inputPipe, outputPipe; try { inputPipe = Pipe.open(); outputPipe = Pipe.open(); @@ -77,25 +80,26 @@ public TestingServerlessApplication start() { } // Run the request handling on a new thread - new Thread(() -> + serverThread = new Thread(() -> start( Channels.newInputStream(inputPipe.source()), Channels.newOutputStream(outputPipe.sink()) ) - ).start(); + ); + serverThread.start(); // Run the thread that sends requests to the server new Thread(() -> { - while (true) { - try { - Socket socket = serverSocket.accept(); + while (!serverSocket.isClosed()) { + try (Socket socket = serverSocket.accept()) { String request = readInputStream(socket.getInputStream()); serverInput.write(request.getBytes()); serverInput.write(new byte[]{'\n'}); String response = readInputStream(serverOutput); socket.getOutputStream().write(response.getBytes()); - socket.close(); + } catch (java.net.SocketException ignored) { + // Socket closed } catch (IOException e) { throw new UncheckedIOException(e); } @@ -107,15 +111,23 @@ public TestingServerlessApplication start() { @Override public @NonNull ServerlessApplication stop() { + super.stop(); try { serverSocket.close(); - } catch (IOException ignored) { + inputPipe.sink().close(); + inputPipe.source().close(); + outputPipe.sink().close(); + outputPipe.source().close(); + serverThread.interrupt(); + } catch (IOException e) { + throw new UncheckedIOException(e); } - return super.stop(); + return this; } String readInputStream(InputStream inputStream) { BufferedReader input = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder result = new StringBuilder(); boolean body = false; From ac6db614b0868ef8b89ad486a86c0b4ae931c7de Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 26 Jun 2024 11:38:42 -0400 Subject: [PATCH 090/180] Store attributes in the POJA request --- .../java/io/micronaut/http/poja/PojaHttpRequest.java | 12 ++++++++++-- .../poja/rawhttp/RawHttpBasedServletHttpRequest.java | 9 --------- .../http/server/tck/poja/PojaServerTestSuite.java | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index e1a2e348b..500b8cdf4 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -6,13 +6,13 @@ 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.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; @@ -45,6 +45,7 @@ public abstract class PojaHttpRequest implements ServletHttpRequest attributes = new MutableConvertibleValuesMap<>(); public PojaHttpRequest( ConversionService conversionService, @@ -57,6 +58,13 @@ public PojaHttpRequest( @Override public abstract CloseableByteBody 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. * diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index d46908117..e6e9b1689 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -19,8 +19,6 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; 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.HttpMethod; import io.micronaut.http.HttpParameters; @@ -130,13 +128,6 @@ public RawHttpRequest getNativeRequest() { return headers; } - @Override - public @NonNull MutableConvertibleValues getAttributes() { - // Attributes are used for sharing internal data and is not applicable in our case. - // So, return empty map. - return new MutableConvertibleValuesMap<>(); - } - @Override public @NonNull Optional getBody() { return (Optional) getBody(Object.class); diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index 086335551..2a3800d31 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,7 +26,7 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 54 tests of 188 fail + // 47 tests of 188 fail "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", "io.micronaut.http.server.tck.tests.cors.CrossOriginTest", From ce19b0490da8e53df6c09c824e5faa985e03cd5a Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 26 Jun 2024 13:25:30 -0400 Subject: [PATCH 091/180] Use the ServletResponseFactory for creating responses --- .../micronaut/http/poja/PojaBodyBinder.java | 4 +-- .../micronaut/http/poja/PojaHttpRequest.java | 21 +++++++++++++--- .../micronaut/http/poja/PojaHttpResponse.java | 3 +-- .../http/poja/ServerlessApplication.java | 25 +++---------------- .../RawHttpBasedServletHttpRequest.java | 8 +++--- .../RawHttpBasedServletHttpResponse.java | 2 +- .../io.micronaut.http.HttpResponseFactory | 1 + .../server/tck/poja/PojaServerTestSuite.java | 17 ++++++------- 8 files changed, 39 insertions(+), 42 deletions(-) create mode 100644 http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java index 78d147e70..9f12dddf2 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java @@ -80,7 +80,7 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest argument = context.getArgument(); final Class type = argument.getType(); String name = argument.getAnnotationMetadata().stringValue(Body.class).orElse(null); - if (source instanceof PojaHttpRequest pojaHttpRequest) { + if (source instanceof PojaHttpRequest pojaHttpRequest) { if (CharSequence.class.isAssignableFrom(type) && name == null) { return pojaHttpRequest.consumeBody(inputStream -> { try { @@ -127,7 +127,7 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest bindFormData( - PojaHttpRequest servletHttpRequest, String name, ArgumentConversionContext context + PojaHttpRequest servletHttpRequest, String name, ArgumentConversionContext context ) { Optional form = servletHttpRequest.getBody(PojaHttpRequest.CONVERTIBLE_VALUES_ARGUMENT); if (form.isEmpty()) { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index 500b8cdf4..3dc3ee98d 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -17,8 +17,9 @@ import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.poja.fork.netty.QueryStringDecoder; +import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpRequest; -import rawhttp.core.RawHttpRequest; +import io.micronaut.servlet.http.ServletHttpResponse; import java.io.BufferedReader; import java.io.IOException; @@ -39,20 +40,24 @@ * @param The body type * @author Andriy */ -public abstract class PojaHttpRequest implements ServletHttpRequest, ServerHttpRequest { +public abstract class PojaHttpRequest + implements ServletHttpRequest, ServerHttpRequest, ServletExchange { 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 + MediaTypeCodecRegistry codecRegistry, + PojaHttpResponse response ) { this.conversionService = conversionService; this.codecRegistry = codecRegistry; + this.response = response; } @Override @@ -143,6 +148,16 @@ public boolean isFormSubmission() { || 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(); diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java index 20c11f608..3ee2c8133 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java @@ -1,12 +1,11 @@ package io.micronaut.http.poja; import io.micronaut.servlet.http.ServletHttpResponse; -import rawhttp.core.RawHttpResponse; /** * A base class for serverless POJA responses. */ -public abstract class PojaHttpResponse implements ServletHttpResponse, T> { +public abstract class PojaHttpResponse implements ServletHttpResponse { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java index aeca1d76d..6070f97c9 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java @@ -137,13 +137,13 @@ void handleSingleRequest(ServletHttpHandler(in, conversionService, codecRegistry, ioExecutor), - new RawHttpBasedServletHttpResponse(conversionService) + RawHttpBasedServletHttpResponse response = new RawHttpBasedServletHttpResponse<>(conversionService); + RawHttpBasedServletHttpRequest exchange = new RawHttpBasedServletHttpRequest<>( + in, conversionService, codecRegistry, ioExecutor, response ); servletHttpHandler.service(exchange); - RawHttpResponse rawHttpResponse = exchange.getResponse().getNativeResponse(); + RawHttpResponse rawHttpResponse = response.getNativeResponse(); rawHttpResponse.writeTo(out); } @@ -152,21 +152,4 @@ void handleSingleRequest(ServletHttpHandler httpRequest, - RawHttpBasedServletHttpResponse httpResponse - ) implements ServletExchange> { - - @Override - public RawHttpBasedServletHttpRequest getRequest() { - return httpRequest; - } - - @Override - public RawHttpBasedServletHttpResponse getResponse() { - return httpResponse; - } - - } - } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index e6e9b1689..8955930d2 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -35,6 +35,7 @@ import rawhttp.core.RawHttp; import rawhttp.core.RawHttpHeaders; import rawhttp.core.RawHttpRequest; +import rawhttp.core.RawHttpResponse; import rawhttp.core.body.BodyReader; import java.io.ByteArrayInputStream; @@ -61,7 +62,7 @@ /** * @author Sahoo. */ -public class RawHttpBasedServletHttpRequest extends PojaHttpRequest { +public class RawHttpBasedServletHttpRequest extends PojaHttpRequest> { private final RawHttp rawHttp; private final RawHttpRequest rawHttpRequest; private final ByteBody byteBody; @@ -72,9 +73,10 @@ public RawHttpBasedServletHttpRequest( InputStream in, ConversionService conversionService, MediaTypeCodecRegistry codecRegistry, - ExecutorService ioExecutor + ExecutorService ioExecutor, + RawHttpBasedServletHttpResponse response ) { - super(conversionService, codecRegistry); + super(conversionService, codecRegistry, (RawHttpBasedServletHttpResponse) response); this.rawHttp = new RawHttp(); try { rawHttpRequest = rawHttp.parseRequest(in); diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index ff92d8a2c..de2519440 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -43,7 +43,7 @@ /** * @author Sahoo. */ -public class RawHttpBasedServletHttpResponse extends PojaHttpResponse { +public class RawHttpBasedServletHttpResponse extends PojaHttpResponse> { private final ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory new file mode 100644 index 000000000..932eae367 --- /dev/null +++ b/http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory @@ -0,0 +1 @@ +io.micronaut.servlet.http.ServletResponseFactory \ No newline at end of file diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index 2a3800d31..ab565dd0b 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,27 +26,24 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 47 tests of 188 fail + // 23 tests of 188 fail + "io.micronaut.http.server.tck.tests.BodyArgumentTest", + "io.micronaut.http.server.tck.tests.BodyTest", "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", - "io.micronaut.http.server.tck.tests.cors.CrossOriginTest", - "io.micronaut.http.server.tck.tests.ErrorHandlerTest", + "io.micronaut.http.server.tck.tests.ErrorHandlerFluxTest", "io.micronaut.http.server.tck.tests.FilterProxyTest", - "io.micronaut.http.server.tck.tests.FiltersTest", + "io.micronaut.http.server.tck.tests.FluxTest", "io.micronaut.http.server.tck.tests.endpoints.health.HealthTest", - "io.micronaut.http.server.tck.tests.bodywritable.HtmlBodyWritableTest", - "io.micronaut.http.server.tck.tests.filter.HttpServerFilterTest", "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", - "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", "io.micronaut.http.server.tck.tests.OctetTest", - "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", - "io.micronaut.http.server.tck.tests.RemoteAddressTest", "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", - "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", + "io.micronaut.http.server.tck.tests.StreamTest", + "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest" }) public class PojaServerTestSuite { } From de3b54d29ee4c360b953b7b76e30a2d1c6df7269 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 26 Jun 2024 15:48:35 -0400 Subject: [PATCH 092/180] Change body input stream reading - Fix the input stream reading for tests. - Remove the transfer encoding header --- .../micronaut/http/poja/PojaHttpRequest.java | 2 +- .../RawHttpBasedServletHttpResponse.java | 3 ++ .../{ => rawhttp}/ServerlessApplication.java | 4 +-- .../poja/BaseServerlessApplicationSpec.groovy | 3 +- .../server/tck/poja/PojaServerTestSuite.java | 18 ++++++------ .../adapter/TestingServerlessApplication.java | 28 ++++++++++++------- 6 files changed, 33 insertions(+), 25 deletions(-) rename http-poja/src/main/java/io/micronaut/http/poja/{ => rawhttp}/ServerlessApplication.java (97%) diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index 3dc3ee98d..d1093c100 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -223,7 +223,7 @@ public int read(byte[] b, int off, int len) throws IOException { @Override public byte[] readAllBytes() throws IOException { - return stream.readAllBytes(); + return stream.readNBytes((int) (maxSize - size)); } @Override diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index de2519440..4cc4ed983 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -64,6 +64,9 @@ public RawHttpBasedServletHttpResponse(ConversionService conversionService) { @Override public RawHttpResponse getNativeResponse() { headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size())); + if ("chunked".equals(headers.get(HttpHeaders.TRANSFER_ENCODING))) { + headers.remove(HttpHeaders.TRANSFER_ENCODING); + } return new RawHttpResponse<>(null, null, new StatusLine(HttpVersion.HTTP_1_1, code, reason), diff --git a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java similarity index 97% rename from http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java rename to http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java index 6070f97c9..60cdb4e0d 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja; +package io.micronaut.http.poja.rawhttp; import io.micronaut.context.ApplicationContext; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.http.poja.rawhttp.RawHttpBasedServletHttpRequest; -import io.micronaut.http.poja.rawhttp.RawHttpBasedServletHttpResponse; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.runtime.EmbeddedApplication; diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy index 00a15d131..4e5c5680a 100644 --- a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -6,6 +6,7 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.MutableHttpResponse import io.micronaut.http.annotation.Filter import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.http.poja.rawhttp.ServerlessApplication import io.micronaut.runtime.ApplicationConfiguration import io.micronaut.session.Session import io.micronaut.session.SessionStore @@ -32,7 +33,7 @@ abstract class BaseServerlessApplicationSpec extends Specification { TestingServerlessApplication app /** - * An extension of {@link ServerlessApplication} that creates 2 + * An extension of {@link io.micronaut.http.poja.rawhttp.ServerlessApplication} that creates 2 * pipes to communicate with the server and simplifies reading and writing to them. */ @Singleton diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index ab565dd0b..57926736b 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,24 +26,22 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 23 tests of 188 fail - "io.micronaut.http.server.tck.tests.BodyArgumentTest", - "io.micronaut.http.server.tck.tests.BodyTest", - "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", + // 21 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", + // Cors are not supported and should be handled by a proxy "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", - "io.micronaut.http.server.tck.tests.ErrorHandlerFluxTest", + // Unclassified + "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", "io.micronaut.http.server.tck.tests.FilterProxyTest", - "io.micronaut.http.server.tck.tests.FluxTest", "io.micronaut.http.server.tck.tests.endpoints.health.HealthTest", - "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", - "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", "io.micronaut.http.server.tck.tests.OctetTest", "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", - "io.micronaut.http.server.tck.tests.StreamTest", - "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest" }) public class PojaServerTestSuite { } diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java index 3472425b4..46ed4d36a 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java @@ -3,7 +3,7 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.annotation.Replaces; import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.poja.ServerlessApplication; +import io.micronaut.http.poja.rawhttp.ServerlessApplication; import io.micronaut.runtime.ApplicationConfiguration; import jakarta.inject.Singleton; @@ -16,6 +16,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.nio.CharBuffer; +import java.nio.channels.AsynchronousCloseException; import java.nio.channels.Channels; import java.nio.channels.Pipe; import java.util.ArrayList; @@ -24,7 +25,7 @@ import java.util.Random; /** - * An extension of {@link io.micronaut.http.poja.ServerlessApplication} that creates 2 + * An extension of {@link ServerlessApplication} that creates 2 * pipes to communicate with the server and simplifies reading and writing to them. * * @author Andriy Dmytruk @@ -80,12 +81,19 @@ public TestingServerlessApplication start() { } // Run the request handling on a new thread - serverThread = new Thread(() -> - start( + serverThread = new Thread(() -> { + try { + 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 @@ -152,10 +160,6 @@ String readInputStream(InputStream inputStream) { for (int i = 0; i < lines.size(); ++i) { String line = lines.get(i); if (i != 0) { - result.append("\n"); - if (body) { - currentSize += 1; - } lastLine = line; } else { lastLine = lastLine + line; @@ -165,6 +169,10 @@ String readInputStream(InputStream inputStream) { } 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()); } From e329be424cb6136e96c1b21c0d60a178a2925f21 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 26 Jun 2024 17:28:37 -0400 Subject: [PATCH 093/180] More TCK test fixes - Make sure byte data is preserved - Fix getting header that is missing --- .../RawHttpBasedServletHttpRequest.java | 2 +- .../server/tck/poja/PojaServerTestSuite.java | 29 +++++++++---------- .../adapter/TestingServerlessApplication.java | 8 +++-- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index 8955930d2..a342714c3 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -299,7 +299,7 @@ public List getAll(CharSequence name) { @Override public @Nullable String get(CharSequence name) { List all = getAll(name); - return all.isEmpty() ? null : all.get(0); + return all == null || all.isEmpty() ? null : all.get(0); } @Override diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index 57926736b..505bfc5b9 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,22 +26,21 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 21 tests of 188 fail + // 19 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", - // Cors are not supported and should be handled by a proxy - "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", - // Unclassified - "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", - "io.micronaut.http.server.tck.tests.FilterProxyTest", - "io.micronaut.http.server.tck.tests.endpoints.health.HealthTest", - "io.micronaut.http.server.tck.tests.OctetTest", - "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", - "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", - "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", - "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", +// "io.micronaut.http.server.tck.tests.hateoas.JsonErrorSerdeTest", +// "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", +// "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", +// // Cors are not supported and should be handled by a proxy +// "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", +// // See https://github.com/micronaut-projects/micronaut-oracle-cloud/issues/925 +// "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", +// // Unclassified +// "io.micronaut.http.server.tck.tests.FilterProxyTest", +// "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", +// "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", +// "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", +// "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", }) public class PojaServerTestSuite { } diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java index 46ed4d36a..5fe43484a 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java @@ -19,6 +19,7 @@ import java.nio.channels.AsynchronousCloseException; import java.nio.channels.Channels; import java.nio.channels.Pipe; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -105,7 +106,7 @@ public TestingServerlessApplication start() { serverInput.write(new byte[]{'\n'}); String response = readInputStream(serverOutput); - socket.getOutputStream().write(response.getBytes()); + socket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1)); } catch (java.net.SocketException ignored) { // Socket closed } catch (IOException e) { @@ -134,7 +135,8 @@ public TestingServerlessApplication start() { } String readInputStream(InputStream inputStream) { - BufferedReader input = new BufferedReader(new InputStreamReader(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(); @@ -186,7 +188,7 @@ String readInputStream(InputStream inputStream) { } } - return result.toString().replace("\r", ""); + return result.toString(); } private List split(String value) { From d742893d75b38c8d3ab9297073f1dfb4a6aa5bc3 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 27 Jun 2024 16:06:16 -0400 Subject: [PATCH 094/180] Fixing and refactoring - Make the PojaHttpRequest mutable for filters. - Make sure that headers are standardized to upper case. - Allow binding byte[] body. - Move the common testing logic to the http-poja-test module. - Use limiting input stream on the whole body to make sure too many bytes are not read. --- http-poja-test/build.gradle | 27 ++++ .../test}/TestingServerlessApplication.java | 40 +++++- .../test/TestingServerlessEmbeddedServer.java | 91 ++++++++++++ .../http/poja/test/SimpleServerSpec.groovy | 41 ++---- .../micronaut/http/poja/PojaBodyBinder.java | 10 ++ .../micronaut/http/poja/PojaHttpRequest.java | 133 ++---------------- .../RawHttpBasedServletHttpRequest.java | 128 +++++++++++++---- .../RawHttpBasedServletHttpResponse.java | 1 + .../http/poja/util/LimitingInputStream.java | 119 ++++++++++++++++ .../poja/BaseServerlessApplicationSpec.groovy | 31 ---- .../http/poja/SimpleServerSpec.groovy | 2 - settings.gradle | 1 + test-suite-http-server-tck-poja/build.gradle | 4 +- .../test/BaseServerlessApplicationSpec.groovy | 15 -- .../server/tck/poja/PojaServerTestSuite.java | 24 ++-- .../server/tck/poja/PojaServerUnderTest.java | 2 +- 16 files changed, 415 insertions(+), 254 deletions(-) create mode 100644 http-poja-test/build.gradle rename {test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter => http-poja-test/src/main/java/io/micronaut/http/poja/test}/TestingServerlessApplication.java (85%) create mode 100644 http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java rename {test-suite-http-server-tck-poja => http-poja-test}/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy (62%) create mode 100644 http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java delete mode 100644 test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy diff --git a/http-poja-test/build.gradle b/http-poja-test/build.gradle new file mode 100644 index 000000000..3b45587fe --- /dev/null +++ b/http-poja-test/build.gradle @@ -0,0 +1,27 @@ +/* + * 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.micronautHttpPoja) + implementation(mn.micronaut.inject.java) + implementation(mn.micronaut.http.client) + + testImplementation(mn.micronaut.jackson.databind) +} + diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java similarity index 85% rename from test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java rename to http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java index 5fe43484a..0983ecb0b 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/adapter/TestingServerlessApplication.java +++ b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java @@ -1,7 +1,24 @@ -package io.micronaut.http.server.tck.poja.adapter; +/* + * 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. + */ +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.rawhttp.ServerlessApplication; import io.micronaut.runtime.ApplicationConfiguration; @@ -24,6 +41,7 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; /** * An extension of {@link ServerlessApplication} that creates 2 @@ -32,9 +50,11 @@ * @author Andriy Dmytruk */ @Singleton +@Requires(env = Environment.TEST) @Replaces(ServerlessApplication.class) public class TestingServerlessApplication extends ServerlessApplication { + private AtomicBoolean isRunning = new AtomicBoolean(false); private int port; private ServerSocket serverSocket; private OutputStream serverInput; @@ -70,6 +90,9 @@ private void createServerSocket() { @Override public TestingServerlessApplication start() { + if (isRunning.compareAndSet(true, true)) { + return this; // Already running + } createServerSocket(); try { @@ -134,7 +157,16 @@ public TestingServerlessApplication start() { return this; } - String readInputStream(InputStream inputStream) { + @Override + public boolean isRunning() { + return isRunning.get(); + } + + public int getPort() { + return port; + } + + 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)); @@ -205,8 +237,4 @@ private List split(String value) { return result; } - public int getPort() { - return port; - } - } diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java new file mode 100644 index 000000000..64bede89e --- /dev/null +++ b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java @@ -0,0 +1,91 @@ +/* + * 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. + */ +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.runtime.ApplicationConfiguration; +import io.micronaut.runtime.server.EmbeddedServer; +import jakarta.inject.Singleton; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +/** + * An embedded server that uses {@link TestingServerlessApplication} as application. + * It can be used for testing POJA serverless applications the same way a normal micronaut + * server would be tested. + * + *

This class is required because the {@link TestingServerlessApplication} cannot + * extend {@link EmbeddedServer} because of conflicting type arguments.

+ * + * @author Andriy Dmytruk + */ +@Singleton +@Requires(env = Environment.TEST) +@Replaces(TestingServerlessApplication.class) +public record TestingServerlessEmbeddedServer( + TestingServerlessApplication application +) implements EmbeddedServer { + + @Override + public int getPort() { + application.start(); + return application.getPort(); + } + + @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()); + } + + @Override + public ApplicationContext getApplicationContext() { + return application.getApplicationContext(); + } + + @Override + public ApplicationConfiguration getApplicationConfiguration() { + return application.getApplicationConfiguration(); + } + + @Override + public boolean isRunning() { + return application.isRunning(); + } +} diff --git a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy b/http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy similarity index 62% rename from test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy rename to http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy index edbb2c5ac..3ca113392 100644 --- a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy +++ b/http-poja-test/src/test/groovy/io/micronaut/http/poja/test/SimpleServerSpec.groovy @@ -1,29 +1,29 @@ package io.micronaut.http.poja.test -import io.micronaut.context.annotation.Property 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.BlockingHttpClient 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 -@Property(name = "micronaut.security.enabled", value = "false") -class SimpleServerSpec extends BaseServerlessApplicationSpec { +class SimpleServerSpec extends Specification { + @Inject + @Client("/") + HttpClient client void "test GET method"() { - given: - BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() - when: - HttpResponse response = client.exchange(HttpRequest.GET("/test").header("Host", "h")) + HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/test").header("Host", "h")) then: response.status == HttpStatus.OK @@ -32,11 +32,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { } void "test invalid GET method"() { - given: - BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() - when: - HttpResponse response = client.exchange(HttpRequest.GET("/test-invalid").header("Host", "h")) + HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/test-invalid").header("Host", "h")) then: var e = thrown(HttpClientResponseException) @@ -46,11 +43,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { } void "test DELETE method"() { - given: - BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() - when: - HttpResponse response = client.exchange(HttpRequest.DELETE("/test").header("Host", "h")) + HttpResponse response = client.toBlocking().exchange(HttpRequest.DELETE("/test").header("Host", "h")) then: response.status() == HttpStatus.OK @@ -58,11 +52,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { } void "test POST method"() { - given: - BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() - when: - HttpResponse response = client.exchange(HttpRequest.POST("/test/Andriy", null).header("Host", "h")) + HttpResponse response = client.toBlocking().exchange(HttpRequest.POST("/test/Andriy", null).header("Host", "h")) then: response.status() == HttpStatus.CREATED @@ -71,11 +62,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { } void "test POST method with unused body"() { - given: - BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() - when: - HttpResponse response = client.exchange(HttpRequest.POST("/test/unused-body", null).header("Host", "h")) + HttpResponse response = client.toBlocking().exchange(HttpRequest.POST("/test/unused-body", null).header("Host", "h")) then: response.contentType.get() == MediaType.TEXT_PLAIN_TYPE @@ -83,11 +71,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { } void "test PUT method"() { - given: - BlockingHttpClient client = HttpClient.create(new URL("http://localhost:" + app.port)).toBlocking() - when: - HttpResponse response = client.exchange(HttpRequest.PUT("/test/Andriy", null).header("Host", "h")) + HttpResponse response = client.toBlocking().exchange(HttpRequest.PUT("/test/Andriy", null).header("Host", "h")) then: response.status() == HttpStatus.OK diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java index 9f12dddf2..cd9b958c1 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java @@ -94,6 +94,16 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest(e); } }); + } else if (argument.getType().isAssignableFrom(byte[].class) && name == null) { + return pojaHttpRequest.consumeBody(inputStream -> { + try { + byte[] bytes = inputStream.readAllBytes(); + return () -> (Optional) Optional.of(bytes); + } catch (IOException e) { + LOG.debug("Error occurred reading function body: {}", e.getMessage(), e); + return new ConversionFailedBindingResult<>(e); + } + }); } else { final MediaType mediaType = source.getContentType().orElse(MediaType.APPLICATION_JSON_TYPE); if (pojaHttpRequest.isFormSubmission()) { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index d1093c100..f97a3e5e4 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -12,7 +12,10 @@ 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; @@ -41,7 +44,7 @@ * @author Andriy */ public abstract class PojaHttpRequest - implements ServletHttpRequest, ServerHttpRequest, ServletExchange { + implements ServletHttpRequest, ServerHttpRequest, ServletExchange, MutableHttpRequest { public static final Argument CONVERTIBLE_VALUES_ARGUMENT = Argument.of(ConvertibleValues.class); @@ -61,7 +64,7 @@ public PojaHttpRequest( } @Override - public abstract CloseableByteBody byteBody(); + public abstract ByteBody byteBody(); @Override public @NonNull MutableConvertibleValues getAttributes() { @@ -77,12 +80,8 @@ public PojaHttpRequest( * @param The function return value */ public T consumeBody(Function consumer) { - try (CloseableByteBody byteBody = byteBody()) { - InputStream stream = new LimitingInputStream( - byteBody.toInputStream(), - byteBody.expectedLength().orElse(0) - ); - return consumer.apply(stream); + try (CloseableByteBody byteBody = byteBody().split(SplitBackpressureMode.FASTEST)) { + return consumer.apply(byteBody.toInputStream()); } } @@ -129,12 +128,12 @@ inputStream, getCharacterEncoding() @Override public InputStream getInputStream() { - return byteBody().toInputStream(); + return byteBody().split(SplitBackpressureMode.FASTEST).toInputStream(); } @Override public BufferedReader getReader() { - return new BufferedReader(new InputStreamReader(byteBody().toInputStream())); + return new BufferedReader(new InputStreamReader(getInputStream())); } /** @@ -173,118 +172,4 @@ private ConvertibleMultiValues parseFormData(String body) { return new ConvertibleMultiValuesMap(parameterValues, conversionService); } - /** - * A wrapper around input stream that limits the maximum size to be read. - */ - public static class LimitingInputStream extends InputStream { - - private long size; - private final InputStream stream; - private final long maxSize; - - public LimitingInputStream(InputStream stream, long maxSize) { - this.maxSize = maxSize; - this.stream = stream; - } - - @Override - public synchronized void mark(int readlimit) { - stream.mark(readlimit); - } - - @Override - public int read() throws IOException { - return stream.read(); - } - - @Override - public int read(byte[] b) throws IOException { - synchronized(this) { - if (size >= maxSize) { - return -1; - } - int sizeRead = stream.read(b); - size += sizeRead; - return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - synchronized (this) { - if (size >= maxSize) { - return -1; - } - int sizeRead = stream.read(b, off, len); - size += sizeRead + off; - return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; - } - } - - @Override - public byte[] readAllBytes() throws IOException { - return stream.readNBytes((int) (maxSize - size)); - } - - @Override - public byte[] readNBytes(int len) throws IOException { - return stream.readNBytes(len); - } - - @Override - public int readNBytes(byte[] b, int off, int len) throws IOException { - return stream.readNBytes(b, off, len); - } - - @Override - public long skip(long n) throws IOException { - return stream.skip(n); - } - - @Override - public void skipNBytes(long n) throws IOException { - stream.skipNBytes(n); - } - - @Override - public int available() throws IOException { - return stream.available(); - } - - @Override - public void close() throws IOException { - stream.close(); - } - - @Override - public synchronized void reset() throws IOException { - stream.reset(); - } - - @Override - public boolean markSupported() { - return stream.markSupported(); - } - - @Override - public long transferTo(OutputStream out) throws IOException { - return stream.transferTo(out); - } - - @Override - public int hashCode() { - return stream.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return stream.equals(obj); - } - - @Override - public String toString() { - return stream.toString(); - } - } - } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index a342714c3..ea6152b0a 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -19,16 +19,18 @@ 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.HttpHeaders; import io.micronaut.http.HttpMethod; -import io.micronaut.http.HttpParameters; +import io.micronaut.http.MutableHttpHeaders; +import io.micronaut.http.MutableHttpParameters; +import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.body.ByteBody; -import io.micronaut.http.body.ByteBody.SplitBackpressureMode; -import io.micronaut.http.body.CloseableByteBody; 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.util.LimitingInputStream; import io.micronaut.http.simple.cookies.SimpleCookies; import io.micronaut.servlet.http.body.InputStreamByteBody; import rawhttp.cookies.ServerCookieHelper; @@ -90,6 +92,7 @@ public RawHttpBasedServletHttpRequest( InputStream stream = rawHttpRequest.getBody() .map(BodyReader::asRawStream) .orElse(new ByteArrayInputStream(new byte[0])); + stream = new LimitingInputStream(stream, contentLength.orElse(0)); this.byteBody = InputStreamByteBody.create(stream, contentLength, ioExecutor); queryParameters = new RawHttpBasedParameters(getUri().getRawQuery(), conversionService); } @@ -111,7 +114,7 @@ public RawHttpRequest getNativeRequest() { } @Override - public @NonNull HttpParameters getParameters() { + public @NonNull MutableHttpParameters getParameters() { return queryParameters; } @@ -126,7 +129,22 @@ public RawHttpRequest getNativeRequest() { } @Override - public @NonNull HttpHeaders getHeaders() { + public MutableHttpRequest cookie(Cookie cookie) { + throw new RuntimeException("Setting cookies not implemented"); + } + + @Override + public MutableHttpRequest uri(URI uri) { + return null; + } + + @Override + public MutableHttpRequest body(T body) { + return null; + } + + @Override + public @NonNull MutableHttpHeaders getHeaders() { return headers; } @@ -136,8 +154,13 @@ public RawHttpRequest getNativeRequest() { } @Override - public @NonNull CloseableByteBody byteBody() { - return byteBody.split(SplitBackpressureMode.FASTEST); + public @NonNull ByteBody byteBody() { + return byteBody; + } + + @Override + public void setConversionService(@NonNull ConversionService conversionService) { + } public record RawHttpCookie( @@ -245,66 +268,99 @@ private static int compareNullableValue(String first, String second) { } } - public static class RawHttpBasedHeaders implements HttpHeaders { - private final RawHttpHeaders rawHttpHeaders; - private final ConversionService conversionService; + public static class RawHttpBasedHeaders implements MutableHttpHeaders { + private final MutableConvertibleMultiValuesMap headers; private RawHttpBasedHeaders(RawHttpHeaders rawHttpHeaders, ConversionService conversionService) { - this.rawHttpHeaders = rawHttpHeaders; - this.conversionService = conversionService; + this.headers = new MutableConvertibleMultiValuesMap<>((Map) rawHttpHeaders.asMap(), conversionService); } @Override public List getAll(CharSequence name) { - return rawHttpHeaders.get(String.valueOf(name)); + return headers.getAll(toUppercaseAscii(name)); } @Override public @Nullable String get(CharSequence name) { - List all = getAll(name); - return all.isEmpty() ? null : all.get(0); + return headers.get(toUppercaseAscii(name)); } @Override public Set names() { - return rawHttpHeaders.getUniqueHeaderNames(); + return headers.names(); } @Override public Collection> values() { - return rawHttpHeaders.asMap().values(); + return headers.values(); } @Override public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - String header = get(name); - return header == null ? Optional.empty() : conversionService.convert(header, conversionContext); + return headers.get(toUppercaseAscii(name), conversionContext); + } + + @Override + public MutableHttpHeaders add(CharSequence header, CharSequence value) { + headers.add(toUppercaseAscii(header), value == null ? null : value.toString()); + return this; + } + + @Override + public MutableHttpHeaders remove(CharSequence header) { + headers.remove(toUppercaseAscii(header)); + return this; + } + + @Override + public void setConversionService(@NonNull ConversionService conversionService) { + this.headers.setConversionService(conversionService); + } + + private static String toUppercaseAscii(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()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if ('a' <= c && c <= 'z') { + c = (char) (c - 32); + } + result.append(c); + } + return result.toString(); } } - private static class RawHttpBasedParameters implements HttpParameters { - private final Map> queryParams; - private final ConversionService conversionService; + private static class RawHttpBasedParameters implements MutableHttpParameters { + private final MutableConvertibleMultiValuesMap queryParams; private RawHttpBasedParameters(String queryString, ConversionService conversionService) { - queryParams = QueryParametersParser.parseQueryParameters(queryString); - this.conversionService = conversionService; + queryParams = new MutableConvertibleMultiValuesMap<>( + (Map) QueryParametersParser.parseQueryParameters(queryString), + conversionService + ); } @Override public List getAll(CharSequence name) { - return queryParams.get(name.toString()); + return queryParams.getAll(name); } @Override public @Nullable String get(CharSequence name) { - List all = getAll(name); - return all == null || all.isEmpty() ? null : all.get(0); + return queryParams.get(name); } @Override public Set names() { - return queryParams.keySet(); + return queryParams.names(); } @Override @@ -314,8 +370,20 @@ public Collection> values() { @Override public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - String header = get(name); - return header == null ? Optional.empty() : conversionService.convert(header, 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); } static class QueryParametersParser { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index 4cc4ed983..ce253b8c7 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -63,6 +63,7 @@ public RawHttpBasedServletHttpResponse(ConversionService conversionService) { @Override public RawHttpResponse getNativeResponse() { + headers.remove(HttpHeaders.CONTENT_LENGTH); headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size())); if ("chunked".equals(headers.get(HttpHeaders.TRANSFER_ENCODING))) { headers.remove(HttpHeaders.TRANSFER_ENCODING); diff --git a/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java b/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java new file mode 100644 index 000000000..62c5c32e0 --- /dev/null +++ b/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java @@ -0,0 +1,119 @@ +package io.micronaut.http.poja.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * 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; + + public LimitingInputStream(InputStream stream, long maxSize) { + this.maxSize = maxSize; + this.stream = stream; + } + + @Override + public synchronized void mark(int readlimit) { + stream.mark(readlimit); + } + + @Override + public int read() throws IOException { + return stream.read(); + } + + @Override + public int read(byte[] b) throws IOException { + synchronized(this) { + if (size >= maxSize) { + return -1; + } + int sizeRead = stream.read(b); + size += sizeRead; + return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + synchronized (this) { + if (size >= maxSize) { + return -1; + } + int sizeRead = stream.read(b, off, len); + size += sizeRead + off; + return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; + } + } + + @Override + public byte[] readAllBytes() throws IOException { + return stream.readNBytes((int) (maxSize - size)); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + return stream.readNBytes(len); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return stream.readNBytes(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return stream.skip(n); + } + + @Override + public void skipNBytes(long n) throws IOException { + stream.skipNBytes(n); + } + + @Override + public int available() throws IOException { + return stream.available(); + } + + @Override + public void close() throws IOException { + stream.close(); + } + + @Override + public synchronized void reset() throws IOException { + stream.reset(); + } + + @Override + public boolean markSupported() { + return stream.markSupported(); + } + + @Override + public long transferTo(OutputStream out) throws IOException { + return stream.transferTo(out); + } + + @Override + public int hashCode() { + return stream.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return stream.equals(obj); + } + + @Override + public String toString() { + return stream.toString(); + } +} diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy index 4e5c5680a..b6ce6bc09 100644 --- a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -2,20 +2,10 @@ package io.micronaut.http.poja import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Replaces -import io.micronaut.http.HttpRequest -import io.micronaut.http.MutableHttpResponse -import io.micronaut.http.annotation.Filter -import io.micronaut.http.filter.ServerFilterChain import io.micronaut.http.poja.rawhttp.ServerlessApplication import io.micronaut.runtime.ApplicationConfiguration -import io.micronaut.session.Session -import io.micronaut.session.SessionStore -import io.micronaut.session.http.HttpSessionFilter -import io.micronaut.session.http.HttpSessionIdEncoder -import io.micronaut.session.http.HttpSessionIdResolver import jakarta.inject.Inject import jakarta.inject.Singleton -import org.reactivestreams.Publisher import spock.lang.Specification import java.nio.ByteBuffer @@ -23,7 +13,6 @@ 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 */ @@ -110,24 +99,4 @@ abstract class BaseServerlessApplicationSpec extends Specification { } } - /** - * Not sure why this is required - * TODO fix this - * - * @author Andriy - */ - @Filter("**/*") - @Replaces(HttpSessionFilter) - static class DisabledSessionFilter extends HttpSessionFilter { - - DisabledSessionFilter(SessionStore sessionStore, HttpSessionIdResolver[] resolvers, HttpSessionIdEncoder[] encoders) { - super(sessionStore, resolvers, encoders) - } - - @Override - Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - return chain.proceed(request) - } - } - } diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy index a29364c32..0825d8f75 100644 --- a/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.http.poja -import io.micronaut.context.annotation.Property import io.micronaut.core.annotation.NonNull import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType @@ -9,7 +8,6 @@ import io.micronaut.http.annotation.* import io.micronaut.test.extensions.spock.annotation.MicronautTest @MicronautTest -@Property(name = "micronaut.security.enabled", value = "false") class SimpleServerSpec extends BaseServerlessApplicationSpec { void "test GET method"() { diff --git a/settings.gradle b/settings.gradle index 4489cafd8..a031a36a0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ include 'http-server-jetty' include 'http-server-undertow' include 'http-server-tomcat' include 'http-poja' +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' diff --git a/test-suite-http-server-tck-poja/build.gradle b/test-suite-http-server-tck-poja/build.gradle index b5308508d..0556ff0de 100644 --- a/test-suite-http-server-tck-poja/build.gradle +++ b/test-suite-http-server-tck-poja/build.gradle @@ -19,10 +19,8 @@ dependencies { testRuntimeOnly(mnValidation.micronaut.validation) testImplementation(projects.micronautHttpPoja) + testImplementation(projects.micronautHttpPojaTest) testImplementation(mnSerde.micronaut.serde.jackson) testImplementation(mn.micronaut.http.client) - - testImplementation("com.athaydes.rawhttp:rawhttp-core:2.4.1") - testImplementation("com.athaydes.rawhttp:rawhttp-cookies:0.2.1") } diff --git a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy b/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy deleted file mode 100644 index 244cee057..000000000 --- a/test-suite-http-server-tck-poja/src/test/groovy/io/micronaut/http/poja/test/BaseServerlessApplicationSpec.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package io.micronaut.http.poja.test - - -import io.micronaut.http.server.tck.poja.adapter.TestingServerlessApplication -import jakarta.inject.Inject -import spock.lang.Specification -/** - * A base class for serverless application test - */ -abstract class BaseServerlessApplicationSpec extends Specification { - - @Inject - TestingServerlessApplication app - -} diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java index 505bfc5b9..08fdb96e9 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java @@ -26,21 +26,17 @@ }) @SuiteDisplayName("HTTP Server TCK for POJA") @ExcludeClassNamePatterns({ - // 19 tests of 188 fail + // 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", -// // Cors are not supported and should be handled by a proxy -// "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", -// // See https://github.com/micronaut-projects/micronaut-oracle-cloud/issues/925 -// "io.micronaut.http.server.tck.tests.constraintshandler.ControllerConstraintHandlerTest", -// // Unclassified -// "io.micronaut.http.server.tck.tests.FilterProxyTest", -// "io.micronaut.http.server.tck.tests.filter.RequestFilterExceptionHandlerTest", -// "io.micronaut.http.server.tck.tests.filter.RequestFilterTest", -// "io.micronaut.http.server.tck.tests.filter.ResponseFilterTest", -// "io.micronaut.http.server.tck.tests.staticresources.StaticResourceTest", + "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", + // Cors are not supported and should be handled by a proxy + "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", + // Proxying is probably not supported. There is no request concurrency + "io.micronaut.http.server.tck.tests.FilterProxyTest", }) public class PojaServerTestSuite { } diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java index 544282fef..c6d660962 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java +++ b/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java @@ -23,7 +23,7 @@ import io.micronaut.http.HttpResponse; import io.micronaut.http.client.BlockingHttpClient; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.server.tck.poja.adapter.TestingServerlessApplication; +import io.micronaut.http.poja.test.TestingServerlessApplication; import io.micronaut.http.tck.ServerUnderTest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 6a6e4011d9953e413545b2426e5745ef8a2f810d Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 27 Jun 2024 16:36:23 -0400 Subject: [PATCH 095/180] Move the sample --- http-poja-test/build.gradle | 4 +- http-poja/README.md | 5 - http-poja/samples/sample1/README.md | 7 - http-poja/samples/sample1/pom.xml | 208 ------------------ .../META-INF/native-image/reflect-config.json | 33 --- .../native-image/resource-config.json | 16 -- settings.gradle | 1 + test-sample-poja/README.md | 39 ++++ test-sample-poja/build.gradle | 34 +++ .../http/poja/sample}/Application.java | 2 +- .../http/poja/sample/TestController.java | 9 +- .../http/poja/sample/SimpleServerSpec.groovy | 71 ++++++ 12 files changed, 154 insertions(+), 275 deletions(-) delete mode 100644 http-poja/README.md delete mode 100644 http-poja/samples/sample1/README.md delete mode 100644 http-poja/samples/sample1/pom.xml delete mode 100644 http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json delete mode 100644 http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json create mode 100644 test-sample-poja/README.md create mode 100644 test-sample-poja/build.gradle rename {http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1 => test-sample-poja/src/main/java/io/micronaut/http/poja/sample}/Application.java (94%) rename http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java => test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java (89%) create mode 100644 test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy diff --git a/http-poja-test/build.gradle b/http-poja-test/build.gradle index 3b45587fe..dce71ebd0 100644 --- a/http-poja-test/build.gradle +++ b/http-poja-test/build.gradle @@ -19,8 +19,8 @@ plugins { dependencies { implementation(projects.micronautHttpPoja) - implementation(mn.micronaut.inject.java) - implementation(mn.micronaut.http.client) + api(mn.micronaut.inject.java) + api(mn.micronaut.http.client) testImplementation(mn.micronaut.jackson.databind) } diff --git a/http-poja/README.md b/http-poja/README.md deleted file mode 100644 index 759340844..000000000 --- a/http-poja/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Plain Old Java Application (POJA) using Micronaut HTTP Framework - -This 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). - diff --git a/http-poja/samples/sample1/README.md b/http-poja/samples/sample1/README.md deleted file mode 100644 index 8e1f5d332..000000000 --- a/http-poja/samples/sample1/README.md +++ /dev/null @@ -1,7 +0,0 @@ -export JAVA_HOME=`java_home v21` -mvn clean package native:compile -java -jar target/*.jar -GET / HTTP/1.1 -Host: h - - diff --git a/http-poja/samples/sample1/pom.xml b/http-poja/samples/sample1/pom.xml deleted file mode 100644 index d35027b48..000000000 --- a/http-poja/samples/sample1/pom.xml +++ /dev/null @@ -1,208 +0,0 @@ - - 4.0.0 - - io.micronaut.samples - http-poja-sample1 - 1.0-SNAPSHOT - jar - - ${project.artifactId} - - - UTF-8 - 21 - 4.4.3 - 2.9.0 - 0.10.2 - io.micronaut.http.poja.sample1.Application - false - - true - - - - - - io.micronaut.platform - micronaut-platform - 4.4.2 - pom - import - - - io.micronaut.servlet - micronaut-http-poja - 4.2.0-SNAPSHOT - - - - - - - - io.micronaut - micronaut-inject - - - io.micronaut.serde - micronaut-serde-jackson - - - io.micronaut.servlet - micronaut-http-poja - - - - org.slf4j - slf4j-simple - - - - - - - org.junit.jupiter - junit-jupiter-api - 5.10.2 - test - - - org.junit.jupiter - junit-jupiter-engine - 5.10.2 - test - - - org.apache.httpcomponents - httpclient - 4.5.14 - test - - - - - - - - kr.motd.maven - os-maven-plugin - 1.7.0 - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - io.micronaut.maven - micronaut-maven-plugin - - aot-${packaging}.properties - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 17 - 17 - true - - - - io.micronaut - micronaut-inject-java - ${micronaut.core.version} - - - - io.micronaut.serde - micronaut-serde-processor - ${micronaut.serialization.version} - - - io.micronaut - micronaut-inject - - - - - true - - -Amicronaut.processing.group=sahoo.graalos.progmodel.mn - -Amicronaut.processing.module=default - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 2.8 - - - copy-dependencies - prepare-package - - copy-dependencies - - - ${project.build.directory}/lib - runtime - - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - ${mainClass} - true - lib/ - - - - - - org.graalvm.buildtools - native-maven-plugin - ${native.maven.plugin.version} - - - ${artifactId}-${os.detected.classifier}-${version} - false - - - - --gc=serial - - --install-exit-handlers - - - ${nativeDryRun} - - ${quickBuild} - true - - - - - - diff --git a/http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json b/http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json deleted file mode 100644 index 2b5826e46..000000000 --- a/http-poja/samples/sample1/src/main/resources/META-INF/native-image/reflect-config.json +++ /dev/null @@ -1,33 +0,0 @@ -[ - { - "name": "io.micronaut.http.poja.sample1.$MyResource$Definition", - "methods": [ - { - "name": "", - "parameterTypes": [] - } - ] - }, - { - "name": "java.security.SecureRandomParameters" - }, - { - "name": "sun.security.provider.NativePRNG$NonBlocking", - "methods": [ - { - "name": "", - "parameterTypes": [] - }, - { - "name": "", - "parameterTypes": [ - "java.security.SecureRandomParameters" - ] - } - ] - }, - { - "name": "java.io.FileDescriptor", - "allDeclaredConstructors": true - } -] diff --git a/http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json b/http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json deleted file mode 100644 index 384c2ef21..000000000 --- a/http-poja/samples/sample1/src/main/resources/META-INF/native-image/resource-config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "resources": { - "includes": [ - { - "pattern": "\\QMETA-INF/services/java.nio.file.spi.FileSystemProvider\\E" - }, - { - "pattern": "\\QMETA-INF/services/rawhttp.core.body.encoding.HttpMessageDecoder\\E" - }, - { - "pattern": "\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E" - } - ] - }, - "bundles": [] -} diff --git a/settings.gradle b/settings.gradle index a031a36a0..6e16e2ef7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -38,3 +38,4 @@ include 'test-suite-http-server-tck-undertow' include 'test-suite-http-server-tck-jetty' include 'test-suite-http-server-tck-poja' include 'test-suite-kotlin-jetty' +include 'test-sample-poja' diff --git a/test-sample-poja/README.md b/test-sample-poja/README.md new file mode 100644 index 000000000..9d57e1c32 --- /dev/null +++ b/test-sample-poja/README.md @@ -0,0 +1,39 @@ +## 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 new file mode 100644 index 000000000..583860007 --- /dev/null +++ b/test-sample-poja/build.gradle @@ -0,0 +1,34 @@ +/* + * 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("application") +} + +dependencies { + implementation(projects.micronautHttpPoja) + implementation(mnLogging.slf4j.simple) + implementation(mn.micronaut.jackson.databind) + + testImplementation(projects.micronautHttpPojaTest) + testImplementation(mn.micronaut.jackson.databind) +} + +run { + mainClass.set("io.micronaut.http.poja.sample.Application") + standardInput = System.in + standardOutput = System.out +} diff --git a/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java similarity index 94% rename from http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java rename to test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java index b9107bb3d..1f43a2e82 100644 --- a/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/Application.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/Application.java @@ -1,4 +1,4 @@ -package io.micronaut.http.poja.sample1; +package io.micronaut.http.poja.sample; import io.micronaut.runtime.Micronaut; diff --git a/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java similarity index 89% rename from http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java rename to test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index e4848e9d5..6ae751f2e 100644 --- a/http-poja/samples/sample1/src/main/java/io/micronaut/http/poja/sample1/MyResource.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -1,4 +1,4 @@ -package io.micronaut.http.poja.sample1; +package io.micronaut.http.poja.sample; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpStatus; @@ -6,16 +6,18 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Delete; import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Patch; 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 MyResource { +public class TestController { + @Get public String index() { return "Hello, Micronaut Without Netty!\n"; @@ -37,5 +39,6 @@ public String create(@NonNull String name) { public 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 new file mode 100644 index 000000000..8fc6cbd2a --- /dev/null +++ b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy @@ -0,0 +1,71 @@ +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" + } + +} From c09604ad2dbcfb6079aeb7227037d026ccd51ee1 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 27 Jun 2024 17:04:45 -0400 Subject: [PATCH 096/180] Copy QueryStringDecoderTest from netty --- http-poja/build.gradle | 1 - .../micronaut/http/poja/PojaHttpRequest.java | 3 +- .../netty => util}/QueryStringDecoder.java | 8 +- .../poja/BaseServerlessApplicationSpec.groovy | 4 +- .../poja/util/QueryStringDecoderTest.groovy | 465 ++++++++++++++++++ 5 files changed, 476 insertions(+), 5 deletions(-) rename http-poja/src/main/java/io/micronaut/http/poja/{fork/netty => util}/QueryStringDecoder.java (98%) create mode 100644 http-poja/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy diff --git a/http-poja/build.gradle b/http-poja/build.gradle index 408777af9..e57e1faa6 100644 --- a/http-poja/build.gradle +++ b/http-poja/build.gradle @@ -27,4 +27,3 @@ dependencies { testImplementation(mnSerde.micronaut.serde.jackson) } - diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index f97a3e5e4..ff02d120b 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -19,7 +19,7 @@ import io.micronaut.http.body.CloseableByteBody; import io.micronaut.http.codec.MediaTypeCodec; import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.http.poja.fork.netty.QueryStringDecoder; +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; @@ -28,7 +28,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.util.Iterator; import java.util.List; import java.util.Map; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java b/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java similarity index 98% rename from http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java rename to http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java index 9a524139a..1f31893ce 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/fork/netty/QueryStringDecoder.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package io.micronaut.http.poja.fork.netty; +package io.micronaut.http.poja.util; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.StringUtils; @@ -52,6 +52,12 @@ * 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 + * . + *

*/ public class QueryStringDecoder { diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy index b6ce6bc09..37b3f00f8 100644 --- a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -95,7 +95,9 @@ abstract class BaseServerlessApplicationSpec extends Specification { var result = readInfo.toString().substring(lastIndex) lastIndex += result.length() - return result.replace('\r', '') + return result + .replace('\r', '') + .replaceAll("Date: .*\n", "") } } diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy new file mode 100644 index 000000000..53227b1ba --- /dev/null +++ b/http-poja/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy @@ -0,0 +1,465 @@ +/* + * 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() == "" + } + +} From bb495b0921f6fb0512e6dd8c743fb850d8391622 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 27 Jun 2024 17:08:42 -0400 Subject: [PATCH 097/180] Remove TODO --- .../io/micronaut/http/poja/rawhttp/ServerlessApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java index 60cdb4e0d..463866bef 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java @@ -57,8 +57,6 @@ public class ServerlessApplication implements EmbeddedApplication> createExchange( @Override public @NonNull ServerlessApplication 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); From c600aae9666d14c0722b43aa64b869b4ad640a0d Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 28 Jun 2024 10:22:24 -0400 Subject: [PATCH 098/180] Spotless and binary compatability fixes --- http-poja-test/build.gradle | 5 +++++ .../test/TestingServerlessApplication.java | 2 +- .../test/TestingServerlessEmbeddedServer.java | 2 +- http-poja/build.gradle | 6 ++++++ .../micronaut/http/poja/PojaHttpRequest.java | 15 +++++++++++++++ .../micronaut/http/poja/PojaHttpResponse.java | 15 +++++++++++++++ .../RawHttpBasedServletHttpRequest.java | 2 +- .../RawHttpBasedServletHttpResponse.java | 2 +- .../poja/rawhttp/ServerlessApplication.java | 2 +- .../http/poja/util/LimitingInputStream.java | 15 +++++++++++++++ .../http/poja/util/QueryStringDecoder.java | 18 +++++++++--------- test-sample-poja/build.gradle | 6 ++++++ .../http/poja/sample/Application.java | 15 +++++++++++++++ .../http/poja/sample/TestController.java | 15 +++++++++++++++ test-suite-http-server-tck-poja/build.gradle | 5 +++++ 15 files changed, 111 insertions(+), 14 deletions(-) diff --git a/http-poja-test/build.gradle b/http-poja-test/build.gradle index dce71ebd0..e19bf21fc 100644 --- a/http-poja-test/build.gradle +++ b/http-poja-test/build.gradle @@ -25,3 +25,8 @@ dependencies { testImplementation(mn.micronaut.jackson.databind) } +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java index 0983ecb0b..98326cf59 100644 --- a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java +++ b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Oracle and/or its affiliates. + * 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. diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java index 64bede89e..b1ef0c945 100644 --- a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java +++ b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Oracle and/or its affiliates. + * 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. diff --git a/http-poja/build.gradle b/http-poja/build.gradle index e57e1faa6..281d6077a 100644 --- a/http-poja/build.gradle +++ b/http-poja/build.gradle @@ -27,3 +27,9 @@ dependencies { testImplementation(mnSerde.micronaut.serde.jackson) } + +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index ff02d120b..7ae8caae1 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java index 3ee2c8133..541ab2a78 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index ea6152b0a..5e0c53e58 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Oracle and/or its affiliates. + * 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. diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index ce253b8c7..61c9bff98 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Oracle and/or its affiliates. + * 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. diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java index 463866bef..69ed8d3e3 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Oracle and/or its affiliates. + * 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. diff --git a/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java b/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java index 62c5c32e0..2cc913bac 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java b/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java index 1f31893ce..bb2d2e716 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java @@ -1,17 +1,17 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2017-2012 original authors * - * 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: + * 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 + * 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. + * 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; diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index 583860007..b5d625aa2 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -32,3 +32,9 @@ run { standardInput = System.in standardOutput = System.out } + +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} 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 index 1f43a2e82..ed172a79a 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index 6ae751f2e..5712421d0 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/test-suite-http-server-tck-poja/build.gradle b/test-suite-http-server-tck-poja/build.gradle index 0556ff0de..3126844da 100644 --- a/test-suite-http-server-tck-poja/build.gradle +++ b/test-suite-http-server-tck-poja/build.gradle @@ -24,3 +24,8 @@ dependencies { testImplementation(mn.micronaut.http.client) } +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} From e991d6d5c2de885b1de8962196b6c24771ef1c51 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 28 Jun 2024 11:30:24 -0400 Subject: [PATCH 099/180] Fix checkstyle --- config/checkstyle/suppressions.xml | 2 ++ .../test/TestingServerlessApplication.java | 7 +++- .../micronaut/http/poja/PojaHttpRequest.java | 3 ++ .../micronaut/http/poja/PojaHttpResponse.java | 4 ++- .../RawHttpBasedServletHttpRequest.java | 35 ++++++++++++++----- .../RawHttpBasedServletHttpResponse.java | 2 ++ .../poja/rawhttp/ServerlessApplication.java | 23 ++++++++++-- .../http/poja/util/LimitingInputStream.java | 2 +- .../http/poja/sample/TestController.java | 10 +++--- 9 files changed, 70 insertions(+), 18 deletions(-) diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 73f71b3a4..f5e7b7d58 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -9,4 +9,6 @@ + + diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java index 98326cf59..b66638702 100644 --- a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java +++ b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java @@ -162,6 +162,11 @@ public boolean isRunning() { return isRunning.get(); } + /** + * Get the port. + * + * @return The port + */ public int getPort() { return port; } @@ -182,7 +187,7 @@ private String readInputStream(InputStream inputStream) { buffer.clear(); try { int length = input.read(buffer); - if (length < 0 ) { + if (length < 0) { break; } } catch (IOException e) { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java index 7ae8caae1..4dfd40569 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java @@ -55,6 +55,8 @@ * to be reused for body and binding. * * @param The body type + * @param The POJA request type + * @param The POJA response type * @author Andriy */ public abstract class PojaHttpRequest @@ -92,6 +94,7 @@ public PojaHttpRequest( * * @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)) { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java index 541ab2a78..2338d4992 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java @@ -19,9 +19,11 @@ /** * A base class for serverless POJA responses. + * + * @param The body type + * @param The POJA response type */ public abstract class PojaHttpResponse implements ServletHttpResponse { - } diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java index 5e0c53e58..bd689c10b 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java @@ -63,6 +63,8 @@ /** * @author Sahoo. + * + * @param Body type */ public class RawHttpBasedServletHttpRequest extends PojaHttpRequest> { private final RawHttp rawHttp; @@ -163,6 +165,11 @@ public void setConversionService(@NonNull ConversionService conversionService) { } + /** + * An implementation of cookie. + * + * @param cookie The internal cookie + */ public record RawHttpCookie( HttpCookie cookie ) implements Cookie { @@ -268,11 +275,17 @@ private static int compareNullableValue(String first, String second) { } } - public static class RawHttpBasedHeaders implements MutableHttpHeaders { - private final MutableConvertibleMultiValuesMap headers; + /** + * Headers implementation. + * + * @param headers The values + */ + public record RawHttpBasedHeaders( + MutableConvertibleMultiValuesMap headers + ) implements MutableHttpHeaders { - private RawHttpBasedHeaders(RawHttpHeaders rawHttpHeaders, ConversionService conversionService) { - this.headers = new MutableConvertibleMultiValuesMap<>((Map) rawHttpHeaders.asMap(), conversionService); + public RawHttpBasedHeaders(RawHttpHeaders rawHttpHeaders, ConversionService conversionService) { + this(new MutableConvertibleMultiValuesMap<>((Map) rawHttpHeaders.asMap(), conversionService)); } @Override @@ -338,14 +351,20 @@ private static String toUppercaseAscii(CharSequence charSequence) { } } - private static class RawHttpBasedParameters implements MutableHttpParameters { - private final MutableConvertibleMultiValuesMap queryParams; + /** + * Query parameters implementation. + * + * @param queryParams The values + */ + private record RawHttpBasedParameters( + MutableConvertibleMultiValuesMap queryParams + ) implements MutableHttpParameters { private RawHttpBasedParameters(String queryString, ConversionService conversionService) { - queryParams = new MutableConvertibleMultiValuesMap<>( + this(new MutableConvertibleMultiValuesMap<>( (Map) QueryParametersParser.parseQueryParameters(queryString), conversionService - ); + )); } @Override diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java index 61c9bff98..ddccd38e5 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java @@ -42,6 +42,8 @@ /** * @author Sahoo. + * + * @param The body type */ public class RawHttpBasedServletHttpResponse extends PojaHttpResponse> { diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java index 69ed8d3e3..5ee363e17 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java @@ -60,6 +60,7 @@ public ServerlessApplication(ApplicationContext applicationContext, this.applicationContext = applicationContext; this.applicationConfiguration = applicationConfiguration; } + @Override public ApplicationContext getApplicationContext() { return applicationContext; @@ -76,7 +77,7 @@ public boolean isRunning() { } /** - * Run the application using a particular channel + * Run the application using a particular channel. * * @param input The input stream * @param output The output stream @@ -118,7 +119,16 @@ protected ServletExchange> createExchange( } } - void runIndefinitely(ServletHttpHandler> servletHttpHandler, + /** + * A method to start the application in a loop. + * + * @param servletHttpHandler The handler + * @param applicationContext The application context + * @param in The input stream + * @param out The output stream + * @throws IOException IO exception + */ + protected void runIndefinitely(ServletHttpHandler> servletHttpHandler, ApplicationContext applicationContext, InputStream in, OutputStream out) throws IOException { @@ -127,6 +137,15 @@ void runIndefinitely(ServletHttpHandler> s } } + /** + * Handle a single request. + * + * @param servletHttpHandler The handler + * @param applicationContext The application context + * @param in The input stream + * @param out The output stream + * @throws IOException IO exception + */ void handleSingleRequest(ServletHttpHandler> servletHttpHandler, ApplicationContext applicationContext, InputStream in, diff --git a/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java b/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java index 2cc913bac..869121de8 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java +++ b/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java @@ -45,7 +45,7 @@ public int read() throws IOException { @Override public int read(byte[] b) throws IOException { - synchronized(this) { + synchronized (this) { if (size >= maxSize) { return -1; } diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index 5712421d0..923cd366f 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -26,7 +26,7 @@ import io.micronaut.http.annotation.Status; /** - * A controller for testing + * A controller for testing. * * @author Sahoo. */ @@ -34,24 +34,24 @@ public class TestController { @Get - public String index() { + public final String index() { return "Hello, Micronaut Without Netty!\n"; } @Delete - public void delete() { + public final void delete() { System.err.println("Delete called"); } @Post("/{name}") @Status(HttpStatus.CREATED) - public String create(@NonNull String name) { + public final String create(@NonNull String name) { return "Hello, " + name + "\n"; } @Put("/{name}") @Status(HttpStatus.OK) - public String update(@NonNull String name) { + public final String update(@NonNull String name) { return "Hello, " + name + "!\n"; } From c453168c31d56194b99fd9a377336bf3c3a15026 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 9 Aug 2024 11:02:58 -0400 Subject: [PATCH 100/180] Decouple POJA into a common and implementation module --- http-poja-common/build.gradle | 33 +++++ .../http/poja/PojaBinderRegistry.java | 0 .../micronaut/http/poja/PojaBodyBinder.java | 0 .../micronaut/http/poja/PojaHttpRequest.java | 0 .../micronaut/http/poja/PojaHttpResponse.java | 0 .../http/poja/util/LimitingInputStream.java | 0 .../http/poja/util/QueryStringDecoder.java | 0 .../io.micronaut.http.HttpResponseFactory | 0 .../poja/BaseServerlessApplicationSpec.groovy | 0 .../http/poja/SimpleServerSpec.groovy | 0 .../poja/util/QueryStringDecoderTest.groovy | 0 .../src/test/resources/logback.xml | 0 {http-poja => http-poja-llhttp}/build.gradle | 2 +- .../RawHttpBasedServletHttpRequest.java | 0 .../RawHttpBasedServletHttpResponse.java | 0 .../poja/rawhttp/ServerlessApplication.java | 0 .../io.micronaut.http.HttpResponseFactory | 1 + .../poja/BaseServerlessApplicationSpec.groovy | 104 ++++++++++++++ .../http/poja/SimpleServerSpec.groovy | 130 ++++++++++++++++++ .../src/test/resources/logback.xml | 16 +++ http-poja-test/build.gradle | 2 +- settings.gradle | 3 +- test-sample-poja/build.gradle | 2 +- .../http/poja/sample/TestController.java | 3 +- test-suite-http-server-tck-poja/build.gradle | 2 +- 25 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 http-poja-common/build.gradle rename {http-poja => http-poja-common}/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java (100%) rename {http-poja => http-poja-common}/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java (100%) rename {http-poja => http-poja-common}/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java (100%) rename {http-poja => http-poja-common}/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java (100%) rename {http-poja => http-poja-common}/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java (100%) rename {http-poja => http-poja-common}/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java (100%) rename {http-poja => http-poja-common}/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory (100%) rename {http-poja => http-poja-common}/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy (100%) rename {http-poja => http-poja-common}/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy (100%) rename {http-poja => http-poja-common}/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy (100%) rename {http-poja => http-poja-common}/src/test/resources/logback.xml (100%) rename {http-poja => http-poja-llhttp}/build.gradle (95%) rename {http-poja => http-poja-llhttp}/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java (100%) rename {http-poja => http-poja-llhttp}/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java (100%) rename {http-poja => http-poja-llhttp}/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java (100%) create mode 100644 http-poja-llhttp/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory create mode 100644 http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy create mode 100644 http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy create mode 100644 http-poja-llhttp/src/test/resources/logback.xml diff --git a/http-poja-common/build.gradle b/http-poja-common/build.gradle new file mode 100644 index 000000000..bd8617cab --- /dev/null +++ b/http-poja-common/build.gradle @@ -0,0 +1,33 @@ +/* + * 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(mnSerde.micronaut.serde.jackson) +} + +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/PojaBinderRegistry.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/PojaBodyBinder.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpRequest.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpResponse.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java b/http-poja-common/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/util/LimitingInputStream.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java b/http-poja-common/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/util/QueryStringDecoder.java diff --git a/http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja-common/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory similarity index 100% rename from http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory rename to http-poja-common/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy similarity index 100% rename from http-poja/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy rename to http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy similarity index 100% rename from http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy rename to http-poja-common/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy diff --git a/http-poja/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy similarity index 100% rename from http-poja/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy rename to http-poja-common/src/test/groovy/io/micronaut/http/poja/util/QueryStringDecoderTest.groovy diff --git a/http-poja/src/test/resources/logback.xml b/http-poja-common/src/test/resources/logback.xml similarity index 100% rename from http-poja/src/test/resources/logback.xml rename to http-poja-common/src/test/resources/logback.xml diff --git a/http-poja/build.gradle b/http-poja-llhttp/build.gradle similarity index 95% rename from http-poja/build.gradle rename to http-poja-llhttp/build.gradle index 281d6077a..f38615115 100644 --- a/http-poja/build.gradle +++ b/http-poja-llhttp/build.gradle @@ -18,7 +18,7 @@ plugins { } dependencies { - api(projects.micronautServletCore) + api(projects.micronautHttpPojaCommon) implementation(libs.rawhttp.core) implementation(libs.rawhttp.cookies) diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java rename to http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java rename to http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java b/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java rename to http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java diff --git a/http-poja-llhttp/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja-llhttp/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory new file mode 100644 index 000000000..932eae367 --- /dev/null +++ b/http-poja-llhttp/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory @@ -0,0 +1 @@ +io.micronaut.servlet.http.ServletResponseFactory \ No newline at end of file diff --git a/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy new file mode 100644 index 000000000..37b3f00f8 --- /dev/null +++ b/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -0,0 +1,104 @@ +package io.micronaut.http.poja + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Replaces +import io.micronaut.http.poja.rawhttp.ServerlessApplication +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 io.micronaut.http.poja.rawhttp.ServerlessApplication} that creates 2 + * pipes to communicate with the server and simplifies reading and writing to them. + */ + @Singleton + @Replaces(ServerlessApplication.class) + static class TestingServerlessApplication extends ServerlessApplication { + + 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 + ServerlessApplication 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-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy new file mode 100644 index 000000000..0825d8f75 --- /dev/null +++ b/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy @@ -0,0 +1,130 @@ +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-Type: text/plain + Content-Length: 32 + + 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-Type: application/json + Content-Length: 148 + + {"_links":{"self":[{"href":"http://h/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".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-Type: text/plain + Content-Length: 13 + + 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-Type: text/plain + Content-Length: 15 + + 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-llhttp/src/test/resources/logback.xml b/http-poja-llhttp/src/test/resources/logback.xml new file mode 100644 index 000000000..ef2b2f918 --- /dev/null +++ b/http-poja-llhttp/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + 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 index e19bf21fc..103af29ca 100644 --- a/http-poja-test/build.gradle +++ b/http-poja-test/build.gradle @@ -18,7 +18,7 @@ plugins { } dependencies { - implementation(projects.micronautHttpPoja) + implementation(projects.micronautHttpPojaLlhttp) api(mn.micronaut.inject.java) api(mn.micronaut.http.client) diff --git a/settings.gradle b/settings.gradle index 6e16e2ef7..80b44def8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,7 +31,8 @@ include 'servlet-engine' include 'http-server-jetty' include 'http-server-undertow' include 'http-server-tomcat' -include 'http-poja' +include 'http-poja-common' +include 'http-poja-llhttp' include 'http-poja-test' include 'test-suite-http-server-tck-tomcat' include 'test-suite-http-server-tck-undertow' diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index b5d625aa2..5fa235d3f 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -19,7 +19,7 @@ plugins { } dependencies { - implementation(projects.micronautHttpPoja) + implementation(projects.micronautHttpPojaLlhttp) implementation(mnLogging.slf4j.simple) implementation(mn.micronaut.jackson.databind) diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index 923cd366f..94d219b75 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -45,7 +45,8 @@ public final void delete() { @Post("/{name}") @Status(HttpStatus.CREATED) - public final String create(@NonNull String name) { + public final String create(@NonNull String name, HttpRequest request) { + request.getBody(); return "Hello, " + name + "\n"; } diff --git a/test-suite-http-server-tck-poja/build.gradle b/test-suite-http-server-tck-poja/build.gradle index 3126844da..aadc6ad42 100644 --- a/test-suite-http-server-tck-poja/build.gradle +++ b/test-suite-http-server-tck-poja/build.gradle @@ -18,7 +18,7 @@ dependencies { testRuntimeOnly(mnValidation.micronaut.validation) - testImplementation(projects.micronautHttpPoja) + testImplementation(projects.micronautHttpPojaLlhttp) testImplementation(projects.micronautHttpPojaTest) testImplementation(mnSerde.micronaut.serde.jackson) testImplementation(mn.micronaut.http.client) From 69c73aa3ca7cd7e3fc79aa5cd0439e6c1591d05d Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 9 Aug 2024 16:42:55 -0400 Subject: [PATCH 101/180] Begin adding Apache implementation for HTTP POJA --- gradle/libs.versions.toml | 9 +- .../build.gradle | 10 +- .../llhttp/ApacheServerlessApplication.java | 97 ++++ .../poja/llhttp/ApacheServletHttpRequest.java | 396 ++++++++++++++++ .../llhttp/ApacheServletHttpResponse.java | 55 +-- .../io.micronaut.http.HttpResponseFactory | 0 .../poja/BaseServerlessApplicationSpec.groovy | 10 +- .../http/poja/SimpleServerSpec.groovy | 4 +- .../src/test/resources/logback.xml | 0 .../poja/PojaHttpServerlessApplication.java | 52 +-- .../poja/BaseServerlessApplicationSpec.groovy | 4 +- .../RawHttpBasedServletHttpRequest.java | 431 ------------------ http-poja-test/build.gradle | 3 +- ...TestingServerlessEmbeddedApplication.java} | 71 ++- .../test/TestingServerlessEmbeddedServer.java | 91 ---- settings.gradle | 4 +- test-sample-poja/build.gradle | 2 +- .../http/poja/sample/TestController.java | 1 + .../build.gradle | 2 +- .../tck/poja/PojaApacheServerTestSuite.java | 2 +- .../tck/poja/PojaApacheServerUnderTest.java | 12 +- .../PojaApacheServerUnderTestProvider.java | 4 +- ...micronaut.http.tck.ServerUnderTestProvider | 1 + ...micronaut.http.tck.ServerUnderTestProvider | 1 - 24 files changed, 629 insertions(+), 633 deletions(-) rename {http-poja-llhttp => http-poja-apache}/build.gradle (87%) create mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java create mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java rename http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java => http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java (69%) rename {http-poja-llhttp => http-poja-apache}/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory (100%) rename {http-poja-llhttp => http-poja-apache}/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy (90%) rename {http-poja-llhttp => http-poja-apache}/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy (92%) rename {http-poja-llhttp => http-poja-apache}/src/test/resources/logback.xml (100%) rename http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java => http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java (65%) delete mode 100644 http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java rename http-poja-test/src/main/java/io/micronaut/http/poja/test/{TestingServerlessApplication.java => TestingServerlessEmbeddedApplication.java} (79%) delete mode 100644 http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java rename {test-suite-http-server-tck-poja => test-suite-http-server-tck-poja-apache}/build.gradle (94%) rename test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java => test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java (97%) rename test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java => test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java (90%) rename test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java => test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java (85%) create mode 100644 test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider delete mode 100644 test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d051f679b..33d8c34d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ undertow = '2.3.15.Final' tomcat = '10.1.26' bcpkix = "1.70" +managed-apache-http-core5 = "5.2.5" +managed-apache-http-client5 = "5.3.1" managed-jetty = '11.0.22' micronaut-reactor = "3.5.0" @@ -22,8 +24,6 @@ micronaut-validation = "4.7.0" google-cloud-functions = '1.1.0' kotlin = "1.9.25" micronaut-logging = "1.3.0" -rawhttp-core = "2.4.1" -rawhttp-cookies = "0.2.1" # Micronaut micronaut-gradle-plugin = "4.4.2" @@ -48,6 +48,8 @@ junit-platform-engine = { module = "org.junit.platform:junit-platform-suite-engi kotlin-stdlib-jdk8 = { module = 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' } kotlin-reflect = { module = 'org.jetbrains.kotlin:kotlin-reflect' } +apache-http-core5 = { module = 'org.apache.httpcomponents.core5:httpcore5', version.ref = 'managed-apache-http-core5' } +apache-http-client5 = { module = 'org.apache.httpcomponents.client5:httpclient5', version.ref = 'managed-apache-http-client5' } tomcat-embed-core = { module = 'org.apache.tomcat.embed:tomcat-embed-core', version.ref = 'tomcat' } undertow-servlet = { module = 'io.undertow:undertow-servlet', version.ref = 'undertow' } jetty-servlet = { module = 'org.eclipse.jetty:jetty-servlet', version.ref = 'managed-jetty' } @@ -57,9 +59,6 @@ jetty-alpn-conscrypt-server = { module = 'org.eclipse.jetty:jetty-alpn-conscrypt kotest-runner = { module = 'io.kotest:kotest-runner-junit5', version.ref = 'kotest-runner' } bcpkix = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix" } -rawhttp-core = { module = "com.athaydes.rawhttp:rawhttp-core", version.ref = "rawhttp-core" } -rawhttp-cookies = { module = "com.athaydes.rawhttp:rawhttp-cookies", version.ref = "rawhttp-cookies" } - google-cloud-functions = { module = 'com.google.cloud.functions:functions-framework-api', version.ref = 'google-cloud-functions' } # Gradle gradle-micronaut = { module = "io.micronaut.gradle:micronaut-gradle-plugin", version.ref = "micronaut-gradle-plugin" } diff --git a/http-poja-llhttp/build.gradle b/http-poja-apache/build.gradle similarity index 87% rename from http-poja-llhttp/build.gradle rename to http-poja-apache/build.gradle index f38615115..96dcf846a 100644 --- a/http-poja-llhttp/build.gradle +++ b/http-poja-apache/build.gradle @@ -15,16 +15,18 @@ */ plugins { id("io.micronaut.build.internal.servlet.module") + id("idea") } dependencies { api(projects.micronautHttpPojaCommon) - implementation(libs.rawhttp.core) - implementation(libs.rawhttp.cookies) + implementation(libs.apache.http.core5) + implementation(libs.apache.http.client5) compileOnly(mn.reactor) compileOnly(mn.micronaut.json.core) + testImplementation(mnSerde.micronaut.serde.jackson) } @@ -33,3 +35,7 @@ micronautBuild { 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 new file mode 100644 index 000000000..1988caffd --- /dev/null +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java @@ -0,0 +1,97 @@ +/* + * 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.context.annotation.DefaultImplementation; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.http.codec.MediaTypeCodecRegistry; +import io.micronaut.http.poja.PojaHttpServerlessApplication; +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. + */ +@Singleton +public class ApacheServerlessApplication + extends PojaHttpServerlessApplication, ApacheServletHttpResponse> { + + /** + * Default constructor. + * + * @param applicationContext The application context + * @param applicationConfiguration The application configuration + */ + public ApacheServerlessApplication(ApplicationContext applicationContext, + ApplicationConfiguration applicationConfiguration) { + super(applicationContext, applicationConfiguration); + } + + @Override + protected void handleSingleRequest( + ServletHttpHandler, ApacheServletHttpResponse> servletHttpHandler, + ApplicationContext applicationContext, + InputStream in, + OutputStream out + ) throws IOException { + ConversionService conversionService = applicationContext.getConversionService(); + MediaTypeCodecRegistry codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); + ExecutorService ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); + + ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); + ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( + in, conversionService, codecRegistry, ioExecutor, response + ); + + servletHttpHandler.service(exchange); + writeResponse(response.getNativeResponse(), out); + } + + private void writeResponse(ClassicHttpResponse response, OutputStream out) throws IOException { + SessionOutputBuffer buffer = new SessionOutputBufferImpl(8 * 1024); + DefaultHttpResponseWriter responseWriter = new DefaultHttpResponseWriter(); + try { + responseWriter.write(response, buffer, out); + } catch (HttpException e) { + throw new RuntimeException("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/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java new file mode 100644 index 000000000..d1524fcd6 --- /dev/null +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -0,0 +1,396 @@ +/* + * 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.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.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.cookie.SameSite; +import io.micronaut.http.poja.PojaHttpRequest; +import io.micronaut.http.simple.cookies.SimpleCookie; +import io.micronaut.http.simple.cookies.SimpleCookies; +import io.micronaut.servlet.http.body.InputStreamByteBody; +import org.apache.hc.client5.http.cookie.CookieSpec; +import org.apache.hc.client5.http.cookie.MalformedCookieException; +import org.apache.hc.client5.http.impl.cookie.RFC6265CookieSpecFactory; +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.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.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +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. + */ +public class ApacheServletHttpRequest extends PojaHttpRequest { + + private final ClassicHttpRequest request; + + private final HttpMethod method; + private final URI uri; + private final MultiValueHeaders headers; + private final MultiValuesQueryParameters queryParameters; + private final SimpleCookies cookies; + + private final ByteBody byteBody; + + public ApacheServletHttpRequest( + InputStream inputStream, + ConversionService conversionService, + MediaTypeCodecRegistry codecRegistry, + ExecutorService ioExecutor, + ApacheServletHttpResponse response + ) { + super(conversionService, codecRegistry, (ApacheServletHttpResponse) response); + + SessionInputBufferImpl sessionInputBuffer = new SessionInputBufferImpl(8192); + DefaultHttpRequestParser parser = new DefaultHttpRequestParser(); + + try { + request = parser.parse(sessionInputBuffer, inputStream); + } catch (HttpException | IOException e) { + throw new RuntimeException("Could parse HTTP request", e); + } + + method = HttpMethod.parse(request.getMethod()); + try { + uri = request.getUri(); + } catch (URISyntaxException e) { + throw new RuntimeException("Could not get request URI", e); + } + headers = new MultiValueHeaders(request.getHeaders(), conversionService); + queryParameters = new MultiValuesQueryParameters(uri, conversionService); + cookies = parseCookies(request, conversionService); + + long contentLength = getContentLength(); + boolean chunked = headers.get(HttpHeaders.TRANSFER_ENCODING) != null + && headers.get(HttpHeaders.TRANSFER_ENCODING).equalsIgnoreCase("chunked"); + + try { + if (contentLength >= 0 || chunked) { + byteBody = InputStreamByteBody.create( + request.getEntity().getContent(), + contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(), + ioExecutor + ); + } else { + // Empty + byteBody = InputStreamByteBody.create(new ByteArrayInputStream(new byte[0]), OptionalLong.of(0), ioExecutor); + } + } catch (IOException e) { + throw new RuntimeException("Could not get request body", e); + } + } + + @Override + public ClassicHttpRequest getNativeRequest() { + return null; + } + + @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) { + return null; + } + + @Override + public MutableHttpRequest body(T body) { + return null; + } + + @Override + public @NonNull MutableHttpHeaders getHeaders() { + return headers; + } + + @Override + public @NonNull Optional getBody() { + return (Optional) getBody(Object.class); + } + + @Override + public @NonNull ByteBody byteBody() { + return byteBody; + } + + @Override + public void setConversionService(@NonNull ConversionService conversionService) { + + } + + private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { + SimpleCookies cookies = new SimpleCookies(conversionService); + CookieSpec cookieSpec = new RFC6265CookieSpecFactory().create(null); // The code does not use the context + + // Parse cookies from the response headers + for (Header header : request.getHeaders(MultiValueHeaders.COOKIE)) { + try { + var parsedCookies = cookieSpec.parse(header, null); + for (var parsedCookie: parsedCookies) { + cookies.put(parsedCookie.getName(), parseCookie(parsedCookie)); + } + } catch (MalformedCookieException e) { + throw new RuntimeException("The cookie is wrong", e); + } + } + return cookies; + } + + private SimpleCookie parseCookie(org.apache.hc.client5.http.cookie.Cookie cookie) { + SimpleCookie result = new SimpleCookie(cookie.getName(), cookie.getValue()); + if (cookie.containsAttribute(Cookie.ATTRIBUTE_SAME_SITE)) { + switch (cookie.getAttribute(Cookie.ATTRIBUTE_SAME_SITE).toLowerCase(Locale.ENGLISH)) { + case "lax" -> result.sameSite(SameSite.Lax); + case "strict" -> result.sameSite(SameSite.Strict); + case "none" -> result.sameSite(SameSite.None); + default -> {} + } + } + result.maxAge(Long.parseLong(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.MAX_AGE_ATTR))); + result.domain(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.DOMAIN_ATTR)); + result.path(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.PATH_ATTR)); + result.httpOnly(cookie.isHttpOnly()); + result.secure(cookie.isSecure()); + return result; + } + + /** + * Headers implementation. + * + * @param headers The values + */ + public record MultiValueHeaders( + MutableConvertibleMultiValuesMap headers + ) implements MutableHttpHeaders { + + public MultiValueHeaders(Header[] headers, ConversionService conversionService) { + this(convertHeaders(headers, conversionService)); + } + + 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 convertHeaders( + Header[] headers, ConversionService conversionService + ) { + Map> map = new HashMap<>(); + 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 MutableConvertibleMultiValuesMap<>(Collections.emptyMap(), conversionService); + } + + private static MutableConvertibleMultiValuesMap standardizeHeaders( + Map> headers, ConversionService conversionService + ) { + MutableConvertibleMultiValuesMap map + = new MutableConvertibleMultiValuesMap<>(Collections.emptyMap(), 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); + } + result.append(c); + upperCase = c == '-'; + } + return result.toString(); + } + } + + /** + * Query parameters implementation. + * + * @param queryParams The values + */ + private record MultiValuesQueryParameters( + MutableConvertibleMultiValuesMap queryParams + ) implements MutableHttpParameters { + + private MultiValuesQueryParameters(URI uri, ConversionService conversionService) { + this(new MutableConvertibleMultiValuesMap<>(parseQueryParameters(uri), 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); + } + + public static Map> parseQueryParameters(URI uri) { + return new URIBuilder(uri).getQueryParams().stream() + .collect(Collectors.groupingBy( + nameValuePair -> nameValuePair.getName(), + Collectors.mapping(nameValuePair -> nameValuePair.getValue(), Collectors.toList()) + )); + } + + } +} diff --git a/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java similarity index 69% rename from http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java index ddccd38e5..33134ead8 100644 --- a/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpResponse.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.rawhttp; +package io.micronaut.http.poja.llhttp; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -27,11 +27,11 @@ import io.micronaut.http.cookie.Cookie; import io.micronaut.http.poja.PojaHttpResponse; import io.micronaut.http.simple.SimpleHttpHeaders; -import rawhttp.core.HttpVersion; -import rawhttp.core.RawHttpHeaders; -import rawhttp.core.RawHttpResponse; -import rawhttp.core.StatusLine; -import rawhttp.core.body.EagerBodyReader; +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 org.apache.hc.core5.http.message.BasicHttpResponse; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; @@ -41,46 +41,41 @@ import java.util.Optional; /** - * @author Sahoo. + * An implementation of the POJA HTTP response based on Apache. * * @param The body type + * @author Andriy Dmytruk */ -public class RawHttpBasedServletHttpResponse extends PojaHttpResponse> { - - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); +public class ApacheServletHttpResponse extends PojaHttpResponse { private int code = HttpStatus.OK.getCode(); - - private String reason = HttpStatus.OK.getReason(); + 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; - public RawHttpBasedServletHttpResponse(ConversionService conversionService) { + public ApacheServletHttpResponse(ConversionService conversionService) { this.headers = new SimpleHttpHeaders(conversionService); } @Override - public RawHttpResponse getNativeResponse() { + public ClassicHttpResponse getNativeResponse() { headers.remove(HttpHeaders.CONTENT_LENGTH); headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size())); - if ("chunked".equals(headers.get(HttpHeaders.TRANSFER_ENCODING))) { + if ("chunked".equalsIgnoreCase(headers.get(HttpHeaders.TRANSFER_ENCODING))) { headers.remove(HttpHeaders.TRANSFER_ENCODING); } - return new RawHttpResponse<>(null, - null, - new StatusLine(HttpVersion.HTTP_1_1, code, reason), - toRawHttpheaders(), - new EagerBodyReader(out.toByteArray())); - } - private RawHttpHeaders toRawHttpheaders() { - RawHttpHeaders.Builder builder = RawHttpHeaders.newBuilder(); - headers.forEachValue(builder::with); - return builder.build(); + 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 @@ -114,9 +109,9 @@ public Optional getBody() { public MutableHttpResponse status(int code, CharSequence message) { this.code = code; if (message == null) { - this.reason = HttpStatus.getDefaultReason(code); + this.reasonPhrase = HttpStatus.getDefaultReason(code); } else { - this.reason = message.toString(); + this.reasonPhrase = message.toString(); } return this; } @@ -128,7 +123,7 @@ public int code() { @Override public String reason() { - return reason; + return reasonPhrase; } @Override diff --git a/http-poja-llhttp/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory similarity index 100% rename from http-poja-llhttp/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory rename to http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory diff --git a/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy similarity index 90% rename from http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy rename to http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy index 37b3f00f8..1a3964558 100644 --- a/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy +++ b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -2,7 +2,7 @@ package io.micronaut.http.poja import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Replaces -import io.micronaut.http.poja.rawhttp.ServerlessApplication +import io.micronaut.http.poja.llhttp.ApacheServerlessApplication import io.micronaut.runtime.ApplicationConfiguration import jakarta.inject.Inject import jakarta.inject.Singleton @@ -22,12 +22,12 @@ abstract class BaseServerlessApplicationSpec extends Specification { TestingServerlessApplication app /** - * An extension of {@link io.micronaut.http.poja.rawhttp.ServerlessApplication} that creates 2 + * An extension of {@link ApacheServerlessApplication} that creates 2 * pipes to communicate with the server and simplifies reading and writing to them. */ @Singleton - @Replaces(ServerlessApplication.class) - static class TestingServerlessApplication extends ServerlessApplication { + @Replaces(ApacheServerlessApplication.class) + static class TestingServerlessApplication extends ApacheServerlessApplication { OutputStream input Pipe.SourceChannel output @@ -45,7 +45,7 @@ abstract class BaseServerlessApplicationSpec extends Specification { } @Override - ServerlessApplication start() { + ApacheServerlessApplication start() { var inputPipe = Pipe.open() var outputPipe = Pipe.open() input = Channels.newOutputStream(inputPipe.sink()) diff --git a/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy similarity index 92% rename from http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy rename to http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy index 0825d8f75..36ddd3c07 100644 --- a/http-poja-llhttp/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy +++ b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy @@ -40,9 +40,9 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { app.read() == """\ HTTP/1.1 404 Not Found Content-Type: application/json - Content-Length: 148 + Content-Length: 140 - {"_links":{"self":[{"href":"http://h/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".stripIndent() + {"_links":{"self":[{"href":"/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".stripIndent() } void "test DELETE method"() { diff --git a/http-poja-llhttp/src/test/resources/logback.xml b/http-poja-apache/src/test/resources/logback.xml similarity index 100% rename from http-poja-llhttp/src/test/resources/logback.xml rename to http-poja-apache/src/test/resources/logback.xml diff --git a/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java similarity index 65% rename from http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java rename to http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java index 5ee363e17..28cde7e4c 100644 --- a/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/ServerlessApplication.java +++ b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServerlessApplication.java @@ -13,21 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.rawhttp; +package io.micronaut.http.poja; import io.micronaut.context.ApplicationContext; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.runtime.EmbeddedApplication; -import io.micronaut.scheduling.TaskExecutors; import io.micronaut.servlet.http.ServletExchange; import io.micronaut.servlet.http.ServletHttpHandler; -import jakarta.inject.Singleton; -import rawhttp.core.RawHttpRequest; -import rawhttp.core.RawHttpResponse; import java.io.IOException; import java.io.InputStream; @@ -36,15 +29,14 @@ import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; -import java.util.concurrent.ExecutorService; /** - * Implementation of {@link EmbeddedApplication} for POSIX serverless environments. + * A base class for POJA serverless applications. + * It implements {@link EmbeddedApplication} for POSIX serverless environments. * - * @author Sahoo. + * @author Andriy Dmytruk. */ -@Singleton -public class ServerlessApplication implements EmbeddedApplication { +public abstract class PojaHttpServerlessApplication implements EmbeddedApplication { private final ApplicationContext applicationContext; private final ApplicationConfiguration applicationConfiguration; @@ -55,8 +47,8 @@ public class ServerlessApplication implements EmbeddedApplication> servletHttpHandler = + public @NonNull PojaHttpServerlessApplication start(InputStream input, OutputStream output) { + final ServletHttpHandler servletHttpHandler = new ServletHttpHandler<>(applicationContext, null) { @Override - protected ServletExchange> createExchange(RawHttpRequest request, - RawHttpResponse response) { + protected ServletExchange createExchange(Object request, Object response) { throw new UnsupportedOperationException("Not expected in serverless mode."); } }; @@ -101,7 +92,7 @@ protected ServletExchange> createExchange( } @Override - public @NonNull ServerlessApplication start() { + public @NonNull PojaHttpServerlessApplication start() { try { // Default streams to streams based on System.inheritedChannel. // If not possible, use System.in/out. @@ -128,7 +119,7 @@ protected ServletExchange> createExchange( * @param out The output stream * @throws IOException IO exception */ - protected void runIndefinitely(ServletHttpHandler> servletHttpHandler, + protected void runIndefinitely(ServletHttpHandler servletHttpHandler, ApplicationContext applicationContext, InputStream in, OutputStream out) throws IOException { @@ -146,26 +137,13 @@ protected void runIndefinitely(ServletHttpHandler> servletHttpHandler, + protected abstract void handleSingleRequest(ServletHttpHandler servletHttpHandler, ApplicationContext applicationContext, InputStream in, - OutputStream out) throws IOException { - ConversionService conversionService = applicationContext.getConversionService(); - MediaTypeCodecRegistry codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); - ExecutorService ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); - - RawHttpBasedServletHttpResponse response = new RawHttpBasedServletHttpResponse<>(conversionService); - RawHttpBasedServletHttpRequest exchange = new RawHttpBasedServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, response - ); - - servletHttpHandler.service(exchange); - RawHttpResponse rawHttpResponse = response.getNativeResponse(); - rawHttpResponse.writeTo(out); - } + OutputStream out) throws IOException; @Override - public @NonNull ServerlessApplication stop() { + public @NonNull PojaHttpServerlessApplication stop() { return EmbeddedApplication.super.stop(); } diff --git a/http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy index 37b3f00f8..39b0b159c 100644 --- a/http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy +++ b/http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy @@ -2,7 +2,7 @@ package io.micronaut.http.poja import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Replaces -import io.micronaut.http.poja.rawhttp.ServerlessApplication +import io.micronaut.http.poja.llhttp.ServerlessApplication import io.micronaut.runtime.ApplicationConfiguration import jakarta.inject.Inject import jakarta.inject.Singleton @@ -22,7 +22,7 @@ abstract class BaseServerlessApplicationSpec extends Specification { TestingServerlessApplication app /** - * An extension of {@link io.micronaut.http.poja.rawhttp.ServerlessApplication} that creates 2 + * An extension of {@link io.micronaut.http.poja.llhttp.ServerlessApplication} that creates 2 * pipes to communicate with the server and simplifies reading and writing to them. */ @Singleton diff --git a/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java b/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java deleted file mode 100644 index bd689c10b..000000000 --- a/http-poja-llhttp/src/main/java/io/micronaut/http/poja/rawhttp/RawHttpBasedServletHttpRequest.java +++ /dev/null @@ -1,431 +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.rawhttp; - -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.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.util.LimitingInputStream; -import io.micronaut.http.simple.cookies.SimpleCookies; -import io.micronaut.servlet.http.body.InputStreamByteBody; -import rawhttp.cookies.ServerCookieHelper; -import rawhttp.core.RawHttp; -import rawhttp.core.RawHttpHeaders; -import rawhttp.core.RawHttpRequest; -import rawhttp.core.RawHttpResponse; -import rawhttp.core.body.BodyReader; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpCookie; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalLong; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * @author Sahoo. - * - * @param Body type - */ -public class RawHttpBasedServletHttpRequest extends PojaHttpRequest> { - private final RawHttp rawHttp; - private final RawHttpRequest rawHttpRequest; - private final ByteBody byteBody; - private final RawHttpBasedHeaders headers; - private final RawHttpBasedParameters queryParameters; - - public RawHttpBasedServletHttpRequest( - InputStream in, - ConversionService conversionService, - MediaTypeCodecRegistry codecRegistry, - ExecutorService ioExecutor, - RawHttpBasedServletHttpResponse response - ) { - super(conversionService, codecRegistry, (RawHttpBasedServletHttpResponse) response); - this.rawHttp = new RawHttp(); - try { - rawHttpRequest = rawHttp.parseRequest(in); - } catch (IOException e) { - throw new RuntimeException(e); - } - headers = new RawHttpBasedHeaders(rawHttpRequest.getHeaders(), conversionService); - OptionalLong contentLength = rawHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_LENGTH) - .map(Long::parseLong).map(OptionalLong::of).orElse(OptionalLong.empty()); - - InputStream stream = rawHttpRequest.getBody() - .map(BodyReader::asRawStream) - .orElse(new ByteArrayInputStream(new byte[0])); - stream = new LimitingInputStream(stream, contentLength.orElse(0)); - this.byteBody = InputStreamByteBody.create(stream, contentLength, ioExecutor); - queryParameters = new RawHttpBasedParameters(getUri().getRawQuery(), conversionService); - } - - @Override - public RawHttpRequest getNativeRequest() { - return rawHttpRequest; - } - - @Override - public @NonNull Cookies getCookies() { - Map cookiesMap = ServerCookieHelper.readClientCookies(rawHttpRequest) - .stream() - .map(RawHttpCookie::new) - .collect(Collectors.toMap(Cookie::getName, Function.identity())); - SimpleCookies cookies = new SimpleCookies(conversionService); - cookies.putAll(cookiesMap); - return cookies; - } - - @Override - public @NonNull MutableHttpParameters getParameters() { - return queryParameters; - } - - @Override - public @NonNull HttpMethod getMethod() { - return HttpMethod.parse(rawHttpRequest.getMethod()); - } - - @Override - public @NonNull URI getUri() { - return rawHttpRequest.getUri(); - } - - @Override - public MutableHttpRequest cookie(Cookie cookie) { - throw new RuntimeException("Setting cookies not implemented"); - } - - @Override - public MutableHttpRequest uri(URI uri) { - return null; - } - - @Override - public MutableHttpRequest body(T body) { - return null; - } - - @Override - public @NonNull MutableHttpHeaders getHeaders() { - return headers; - } - - @Override - public @NonNull Optional getBody() { - return (Optional) getBody(Object.class); - } - - @Override - public @NonNull ByteBody byteBody() { - return byteBody; - } - - @Override - public void setConversionService(@NonNull ConversionService conversionService) { - - } - - /** - * An implementation of cookie. - * - * @param cookie The internal cookie - */ - public record RawHttpCookie( - HttpCookie cookie - ) implements Cookie { - - @Override - public @NonNull String getName() { - return cookie.getName(); - } - - @Override - public @NonNull String getValue() { - return cookie.getValue(); - } - - @Override - public @Nullable String getDomain() { - return cookie.getDomain(); - } - - @Override - public @Nullable String getPath() { - return cookie.getPath(); - } - - @Override - public boolean isHttpOnly() { - return cookie.isHttpOnly(); - } - - @Override - public boolean isSecure() { - return cookie.getSecure(); - } - - @Override - public long getMaxAge() { - return cookie.getMaxAge(); - } - - @Override - public @NonNull Cookie maxAge(long maxAge) { - cookie.setMaxAge(maxAge); - return this; - } - - @Override - public @NonNull Cookie value(@NonNull String value) { - cookie.setValue(value); - return this; - } - - @Override - public @NonNull Cookie domain(@Nullable String domain) { - cookie.setDomain(domain); - return this; - } - - @Override - public @NonNull Cookie path(@Nullable String path) { - cookie.setPath(path); - return this; - } - - @Override - public @NonNull Cookie secure(boolean secure) { - cookie.setSecure(secure); - return this; - } - - @Override - public @NonNull Cookie httpOnly(boolean httpOnly) { - cookie.setHttpOnly(httpOnly); - return this; - } - - @Override - public int compareTo(Cookie o) { - int v = getName().compareTo(o.getName()); - if (v != 0) { - return v; - } - - v = compareNullableValue(getPath(), o.getPath()); - if (v != 0) { - return v; - } - - return compareNullableValue(getDomain(), o.getDomain()); - } - - private static int compareNullableValue(String first, String second) { - if (first == null) { - if (second != null) { - return -1; - } else { - return 0; - } - } else if (second == null) { - return 1; - } else { - return first.compareToIgnoreCase(second); - } - } - } - - /** - * Headers implementation. - * - * @param headers The values - */ - public record RawHttpBasedHeaders( - MutableConvertibleMultiValuesMap headers - ) implements MutableHttpHeaders { - - public RawHttpBasedHeaders(RawHttpHeaders rawHttpHeaders, ConversionService conversionService) { - this(new MutableConvertibleMultiValuesMap<>((Map) rawHttpHeaders.asMap(), conversionService)); - } - - @Override - public List getAll(CharSequence name) { - return headers.getAll(toUppercaseAscii(name)); - } - - @Override - public @Nullable String get(CharSequence name) { - return headers.get(toUppercaseAscii(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(toUppercaseAscii(name), conversionContext); - } - - @Override - public MutableHttpHeaders add(CharSequence header, CharSequence value) { - headers.add(toUppercaseAscii(header), value == null ? null : value.toString()); - return this; - } - - @Override - public MutableHttpHeaders remove(CharSequence header) { - headers.remove(toUppercaseAscii(header)); - return this; - } - - @Override - public void setConversionService(@NonNull ConversionService conversionService) { - this.headers.setConversionService(conversionService); - } - - private static String toUppercaseAscii(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()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if ('a' <= c && c <= 'z') { - c = (char) (c - 32); - } - result.append(c); - } - return result.toString(); - } - } - - /** - * Query parameters implementation. - * - * @param queryParams The values - */ - private record RawHttpBasedParameters( - MutableConvertibleMultiValuesMap queryParams - ) implements MutableHttpParameters { - - private RawHttpBasedParameters(String queryString, ConversionService conversionService) { - this(new MutableConvertibleMultiValuesMap<>( - (Map) QueryParametersParser.parseQueryParameters(queryString), - 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); - } - - static class QueryParametersParser { - public static Map> parseQueryParameters(String queryParameters) { - return queryParameters != null && !queryParameters.isEmpty() ? - Arrays.stream(queryParameters.split("[&;]")) - .map(QueryParametersParser::splitQueryParameter) - .collect(Collectors.groupingBy(Map.Entry::getKey, - LinkedHashMap::new, - Collectors.mapping(Map.Entry::getValue, Collectors.toList()))) : - Collections.emptyMap(); - } - - private static Map.Entry splitQueryParameter(String parameter) { - int idx = parameter.indexOf("="); - String key = decode(idx > 0 ? parameter.substring(0, idx) : parameter); - String value = idx > 0 && parameter.length() > idx + 1 ? decode(parameter.substring(idx + 1)) : ""; - return new AbstractMap.SimpleImmutableEntry<>(key, value); - } - - private static String decode(String urlEncodedString) { - return URLDecoder.decode(urlEncodedString, StandardCharsets.UTF_8); - } - } - } -} diff --git a/http-poja-test/build.gradle b/http-poja-test/build.gradle index 103af29ca..93906ee35 100644 --- a/http-poja-test/build.gradle +++ b/http-poja-test/build.gradle @@ -18,11 +18,12 @@ plugins { } dependencies { - implementation(projects.micronautHttpPojaLlhttp) + implementation(projects.micronautHttpPojaCommon) api(mn.micronaut.inject.java) api(mn.micronaut.http.client) testImplementation(mn.micronaut.jackson.databind) + testImplementation(projects.micronautHttpPojaApache) } micronautBuild { diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java similarity index 79% rename from http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java rename to http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java index b66638702..72c0cb3e0 100644 --- a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessApplication.java +++ b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedApplication.java @@ -20,8 +20,10 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.poja.rawhttp.ServerlessApplication; +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; @@ -30,8 +32,11 @@ 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; @@ -44,15 +49,21 @@ import java.util.concurrent.atomic.AtomicBoolean; /** - * An extension of {@link ServerlessApplication} that creates 2 - * pipes to communicate with the server and simplifies reading and writing to them. + * 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(ServerlessApplication.class) -public class TestingServerlessApplication extends ServerlessApplication { +@Replaces(EmbeddedApplication.class) +public class TestingServerlessEmbeddedApplication implements EmbeddedServer { + + private PojaHttpServerlessApplication application; private AtomicBoolean isRunning = new AtomicBoolean(false); private int port; @@ -67,11 +78,12 @@ public class TestingServerlessApplication extends ServerlessApplication { /** * Default constructor. * - * @param applicationContext The application context - * @param applicationConfiguration The application configuration + * @param application The application context */ - public TestingServerlessApplication(ApplicationContext applicationContext, ApplicationConfiguration applicationConfiguration) { - super(applicationContext, applicationConfiguration); + public TestingServerlessEmbeddedApplication( + PojaHttpServerlessApplication application + ) { + this.application = application; } private void createServerSocket() { @@ -89,7 +101,7 @@ private void createServerSocket() { } @Override - public TestingServerlessApplication start() { + public TestingServerlessEmbeddedApplication start() { if (isRunning.compareAndSet(true, true)) { return this; // Already running } @@ -107,7 +119,7 @@ public TestingServerlessApplication start() { // Run the request handling on a new thread serverThread = new Thread(() -> { try { - start( + application.start( Channels.newInputStream(inputPipe.source()), Channels.newOutputStream(outputPipe.sink()) ); @@ -142,8 +154,8 @@ public TestingServerlessApplication start() { } @Override - public @NonNull ServerlessApplication stop() { - super.stop(); + public @NonNull TestingServerlessEmbeddedApplication stop() { + application.stop(); try { serverSocket.close(); inputPipe.sink().close(); @@ -171,6 +183,30 @@ 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()); + } + 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)); @@ -242,4 +278,13 @@ private List split(String value) { return result; } + @Override + public ApplicationContext getApplicationContext() { + return application.getApplicationContext(); + } + + @Override + public ApplicationConfiguration getApplicationConfiguration() { + return application.getApplicationConfiguration(); + } } diff --git a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java b/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.java deleted file mode 100644 index b1ef0c945..000000000 --- a/http-poja-test/src/main/java/io/micronaut/http/poja/test/TestingServerlessEmbeddedServer.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.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.runtime.ApplicationConfiguration; -import io.micronaut.runtime.server.EmbeddedServer; -import jakarta.inject.Singleton; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; - -/** - * An embedded server that uses {@link TestingServerlessApplication} as application. - * It can be used for testing POJA serverless applications the same way a normal micronaut - * server would be tested. - * - *

This class is required because the {@link TestingServerlessApplication} cannot - * extend {@link EmbeddedServer} because of conflicting type arguments.

- * - * @author Andriy Dmytruk - */ -@Singleton -@Requires(env = Environment.TEST) -@Replaces(TestingServerlessApplication.class) -public record TestingServerlessEmbeddedServer( - TestingServerlessApplication application -) implements EmbeddedServer { - - @Override - public int getPort() { - application.start(); - return application.getPort(); - } - - @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()); - } - - @Override - public ApplicationContext getApplicationContext() { - return application.getApplicationContext(); - } - - @Override - public ApplicationConfiguration getApplicationConfiguration() { - return application.getApplicationConfiguration(); - } - - @Override - public boolean isRunning() { - return application.isRunning(); - } -} diff --git a/settings.gradle b/settings.gradle index 80b44def8..0fe362eff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,11 +32,11 @@ include 'http-server-jetty' include 'http-server-undertow' include 'http-server-tomcat' include 'http-poja-common' -include 'http-poja-llhttp' +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' +include 'test-suite-http-server-tck-poja-apache' include 'test-suite-kotlin-jetty' include 'test-sample-poja' diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index 5fa235d3f..52cdfd159 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -19,7 +19,7 @@ plugins { } dependencies { - implementation(projects.micronautHttpPojaLlhttp) + implementation(projects.micronautHttpPojaApache) implementation(mnLogging.slf4j.simple) implementation(mn.micronaut.jackson.databind) diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index 94d219b75..b9dee602e 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -16,6 +16,7 @@ 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; diff --git a/test-suite-http-server-tck-poja/build.gradle b/test-suite-http-server-tck-poja-apache/build.gradle similarity index 94% rename from test-suite-http-server-tck-poja/build.gradle rename to test-suite-http-server-tck-poja-apache/build.gradle index aadc6ad42..988fe30c8 100644 --- a/test-suite-http-server-tck-poja/build.gradle +++ b/test-suite-http-server-tck-poja-apache/build.gradle @@ -18,7 +18,7 @@ dependencies { testRuntimeOnly(mnValidation.micronaut.validation) - testImplementation(projects.micronautHttpPojaLlhttp) + testImplementation(projects.micronautHttpPojaApache) testImplementation(projects.micronautHttpPojaTest) testImplementation(mnSerde.micronaut.serde.jackson) testImplementation(mn.micronaut.http.client) diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java similarity index 97% rename from test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java rename to test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java index 08fdb96e9..25278b886 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerTestSuite.java +++ b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java @@ -38,5 +38,5 @@ // Proxying is probably not supported. There is no request concurrency "io.micronaut.http.server.tck.tests.FilterProxyTest", }) -public class PojaServerTestSuite { +public class PojaApacheServerTestSuite { } diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java similarity index 90% rename from test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java rename to test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java index c6d660962..d4af5b18f 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTest.java +++ b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java @@ -23,7 +23,7 @@ import io.micronaut.http.HttpResponse; import io.micronaut.http.client.BlockingHttpClient; import io.micronaut.http.client.HttpClient; -import io.micronaut.http.poja.test.TestingServerlessApplication; +import io.micronaut.http.poja.test.TestingServerlessEmbeddedApplication; import io.micronaut.http.tck.ServerUnderTest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,16 +34,16 @@ import java.util.Map; import java.util.Optional; -public class PojaServerUnderTest implements ServerUnderTest { +public class PojaApacheServerUnderTest implements ServerUnderTest { - private static final Logger LOG = LoggerFactory.getLogger(PojaServerUnderTest.class); + private static final Logger LOG = LoggerFactory.getLogger(PojaApacheServerUnderTest.class); private final ApplicationContext applicationContext; - private final TestingServerlessApplication application; + private final TestingServerlessEmbeddedApplication application; private final BlockingHttpClient client; private final int port; - public PojaServerUnderTest(Map properties) { + 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); @@ -55,7 +55,7 @@ public PojaServerUnderTest(Map properties) { .properties(properties) .deduceEnvironment(false) .start(); - application = applicationContext.findBean(TestingServerlessApplication.class) + application = applicationContext.findBean(TestingServerlessEmbeddedApplication.class) .orElseThrow(() -> new IllegalStateException("TestingServerlessApplication bean is required")); application.start(); port = application.getPort(); diff --git a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java similarity index 85% rename from test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java rename to test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java index 74fb84e10..19c28093e 100644 --- a/test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaServerUnderTestProvider.java +++ b/test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java @@ -20,11 +20,11 @@ import java.util.Map; -public class PojaServerUnderTestProvider implements ServerUnderTestProvider { +public class PojaApacheServerUnderTestProvider implements ServerUnderTestProvider { @Override public ServerUnderTest getServer(Map properties) { - return new PojaServerUnderTest(properties); + return new PojaApacheServerUnderTest(properties); } } 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 new file mode 100644 index 000000000..ed820b5ae --- /dev/null +++ b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider @@ -0,0 +1 @@ +io.micronaut.http.server.tck.poja.PojaApacheServerUnderTestProvider diff --git a/test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider b/test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider deleted file mode 100644 index 960de8c73..000000000 --- a/test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.server.tck.poja.PojaServerUnderTestProvider From 77e1999a7f0d8e65dd2b172e426affec24cf1dac Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 15 Aug 2024 16:39:39 -0400 Subject: [PATCH 102/180] Fix header parsing and body reading for Apache POJA --- .../poja/llhttp/ApacheServletHttpRequest.java | 79 ++++++++++++++++--- .../micronaut/http/poja/PojaHttpRequest.java | 34 ++++---- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index d1524fcd6..010c7defd 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -20,7 +20,6 @@ import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.MutableConvertibleMultiValuesMap; -import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpParameters; @@ -88,7 +87,7 @@ public ApacheServletHttpRequest( ExecutorService ioExecutor, ApacheServletHttpResponse response ) { - super(conversionService, codecRegistry, (ApacheServletHttpResponse) response); + super(conversionService, codecRegistry, response); SessionInputBufferImpl sessionInputBuffer = new SessionInputBufferImpl(8192); DefaultHttpRequestParser parser = new DefaultHttpRequestParser(); @@ -110,19 +109,19 @@ public ApacheServletHttpRequest( cookies = parseCookies(request, conversionService); long contentLength = getContentLength(); - boolean chunked = headers.get(HttpHeaders.TRANSFER_ENCODING) != null - && headers.get(HttpHeaders.TRANSFER_ENCODING).equalsIgnoreCase("chunked"); - + OptionalLong optionalContentLength = contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(); try { - if (contentLength >= 0 || chunked) { - byteBody = InputStreamByteBody.create( - request.getEntity().getContent(), - contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(), - ioExecutor + if (sessionInputBuffer.available() > 0) { + byte[] data = new byte[sessionInputBuffer.available()]; + sessionInputBuffer.read(data, inputStream); + + InputStream combinedStream = new CombinedInputStream( + new ByteArrayInputStream(data), + inputStream ); + byteBody = InputStreamByteBody.create(combinedStream, optionalContentLength, ioExecutor); } else { - // Empty - byteBody = InputStreamByteBody.create(new ByteArrayInputStream(new byte[0]), OptionalLong.of(0), ioExecutor); + byteBody = InputStreamByteBody.create(inputStream, optionalContentLength, ioExecutor); } } catch (IOException e) { throw new RuntimeException("Could not get request body", e); @@ -190,6 +189,60 @@ public void setConversionService(@NonNull ConversionService 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 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(); + } + } + private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { SimpleCookies cookies = new SimpleCookies(conversionService); CookieSpec cookieSpec = new RFC6265CookieSpecFactory().create(null); // The code does not use the context @@ -295,7 +348,7 @@ private static MutableConvertibleMultiValuesMap convertHeaders( } map.get(header.getName()).add(header.getValue()); } - return new MutableConvertibleMultiValuesMap<>(Collections.emptyMap(), conversionService); + return new MutableConvertibleMultiValuesMap<>(map, conversionService); } private static MutableConvertibleMultiValuesMap standardizeHeaders( 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 index 4dfd40569..5fe8e9708 100644 --- 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 @@ -112,21 +112,12 @@ public T consumeBody(Function consumer) { final MediaType contentType = getContentType().orElse(MediaType.APPLICATION_JSON_TYPE); if (isFormSubmission()) { - return consumeBody(inputStream -> { - try { - String content = IOUtils.readText(new BufferedReader(new InputStreamReader( - inputStream, getCharacterEncoding() - ))); - ConvertibleMultiValues form = parseFormData(content); - if (ConvertibleValues.class == type || Object.class == type) { - return Optional.of((T) form); - } else { - return conversionService.convert(form.asMap(), arg); - } - } catch (IOException e) { - throw new RuntimeException("Unable to parse body", e); - } - }); + 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); @@ -143,6 +134,19 @@ inputStream, getCharacterEncoding() } } + 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(); From 72f7915ba3bc1f5dcca4f0cadd5c43f8d0a18773 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 15 Aug 2024 18:53:02 -0400 Subject: [PATCH 103/180] Fix cookie parsing for Apache POJA --- .../poja/llhttp/ApacheServletHttpRequest.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index 010c7defd..aff5ef8ea 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -33,6 +33,7 @@ import io.micronaut.http.simple.cookies.SimpleCookie; import io.micronaut.http.simple.cookies.SimpleCookies; import io.micronaut.servlet.http.body.InputStreamByteBody; +import org.apache.hc.client5.http.cookie.CookieOrigin; import org.apache.hc.client5.http.cookie.CookieSpec; import org.apache.hc.client5.http.cookie.MalformedCookieException; import org.apache.hc.client5.http.impl.cookie.RFC6265CookieSpecFactory; @@ -42,6 +43,7 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.impl.io.DefaultHttpRequestParser; import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.net.URIBuilder; import java.io.ByteArrayInputStream; @@ -245,17 +247,28 @@ public void close() throws IOException { private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { SimpleCookies cookies = new SimpleCookies(conversionService); - CookieSpec cookieSpec = new RFC6265CookieSpecFactory().create(null); // The code does not use the context - // Parse cookies from the response headers + // Manually parse cookies from the response headers for (Header header : request.getHeaders(MultiValueHeaders.COOKIE)) { - try { - var parsedCookies = cookieSpec.parse(header, null); - for (var parsedCookie: parsedCookies) { - cookies.put(parsedCookie.getName(), parseCookie(parsedCookie)); + 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; } - } catch (MalformedCookieException e) { - throw new RuntimeException("The cookie is wrong", e); + } + if (name != null) { + cookies.put(name, Cookie.of(name, cookie.substring(start))); } } return cookies; @@ -271,7 +284,10 @@ private SimpleCookie parseCookie(org.apache.hc.client5.http.cookie.Cookie cookie default -> {} } } - result.maxAge(Long.parseLong(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.MAX_AGE_ATTR))); + String maxAge = cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.MAX_AGE_ATTR); + if (maxAge != null) { + result.maxAge(Long.parseLong(maxAge)); + } result.domain(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.DOMAIN_ATTR)); result.path(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.PATH_ATTR)); result.httpOnly(cookie.isHttpOnly()); From 35c38cce9de7f817f9ae400c2f4fb928d72d900f Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 16 Aug 2024 10:28:01 -0400 Subject: [PATCH 104/180] More fixes for TCK tests - Remove unneeded dependency. - Fix for header reading. - Fix for reading input stream by limiting it with content length. --- gradle/libs.versions.toml | 2 - http-poja-apache/build.gradle | 1 - .../poja/llhttp/ApacheServletHttpRequest.java | 49 +++++-------------- 3 files changed, 13 insertions(+), 39 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33d8c34d9..a25df2299 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,6 @@ tomcat = '10.1.26' bcpkix = "1.70" managed-apache-http-core5 = "5.2.5" -managed-apache-http-client5 = "5.3.1" managed-jetty = '11.0.22' micronaut-reactor = "3.5.0" @@ -49,7 +48,6 @@ kotlin-stdlib-jdk8 = { module = 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' } kotlin-reflect = { module = 'org.jetbrains.kotlin:kotlin-reflect' } apache-http-core5 = { module = 'org.apache.httpcomponents.core5:httpcore5', version.ref = 'managed-apache-http-core5' } -apache-http-client5 = { module = 'org.apache.httpcomponents.client5:httpclient5', version.ref = 'managed-apache-http-client5' } tomcat-embed-core = { module = 'org.apache.tomcat.embed:tomcat-embed-core', version.ref = 'tomcat' } undertow-servlet = { module = 'io.undertow:undertow-servlet', version.ref = 'undertow' } jetty-servlet = { module = 'org.eclipse.jetty:jetty-servlet', version.ref = 'managed-jetty' } diff --git a/http-poja-apache/build.gradle b/http-poja-apache/build.gradle index 96dcf846a..4ee8b902f 100644 --- a/http-poja-apache/build.gradle +++ b/http-poja-apache/build.gradle @@ -21,7 +21,6 @@ plugins { dependencies { api(projects.micronautHttpPojaCommon) implementation(libs.apache.http.core5) - implementation(libs.apache.http.client5) compileOnly(mn.reactor) compileOnly(mn.micronaut.json.core) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index aff5ef8ea..fa9989bcb 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -28,22 +28,16 @@ import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; -import io.micronaut.http.cookie.SameSite; import io.micronaut.http.poja.PojaHttpRequest; -import io.micronaut.http.simple.cookies.SimpleCookie; +import io.micronaut.http.poja.util.LimitingInputStream; import io.micronaut.http.simple.cookies.SimpleCookies; import io.micronaut.servlet.http.body.InputStreamByteBody; -import org.apache.hc.client5.http.cookie.CookieOrigin; -import org.apache.hc.client5.http.cookie.CookieSpec; -import org.apache.hc.client5.http.cookie.MalformedCookieException; -import org.apache.hc.client5.http.impl.cookie.RFC6265CookieSpecFactory; 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.impl.io.DefaultHttpRequestParser; import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; -import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.net.URIBuilder; import java.io.ByteArrayInputStream; @@ -56,7 +50,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -113,18 +106,22 @@ public ApacheServletHttpRequest( long contentLength = getContentLength(); OptionalLong optionalContentLength = contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(); try { + InputStream bodyStream = inputStream; if (sessionInputBuffer.available() > 0) { byte[] data = new byte[sessionInputBuffer.available()]; sessionInputBuffer.read(data, inputStream); - InputStream combinedStream = new CombinedInputStream( + bodyStream = new CombinedInputStream( new ByteArrayInputStream(data), inputStream ); - byteBody = InputStreamByteBody.create(combinedStream, optionalContentLength, ioExecutor); - } else { - byteBody = InputStreamByteBody.create(inputStream, optionalContentLength, ioExecutor); } + if (contentLength >= 0) { + bodyStream = new LimitingInputStream(bodyStream, contentLength); + } + byteBody = InputStreamByteBody.create( + bodyStream, optionalContentLength, ioExecutor + ); } catch (IOException e) { throw new RuntimeException("Could not get request body", e); } @@ -274,27 +271,6 @@ private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService return cookies; } - private SimpleCookie parseCookie(org.apache.hc.client5.http.cookie.Cookie cookie) { - SimpleCookie result = new SimpleCookie(cookie.getName(), cookie.getValue()); - if (cookie.containsAttribute(Cookie.ATTRIBUTE_SAME_SITE)) { - switch (cookie.getAttribute(Cookie.ATTRIBUTE_SAME_SITE).toLowerCase(Locale.ENGLISH)) { - case "lax" -> result.sameSite(SameSite.Lax); - case "strict" -> result.sameSite(SameSite.Strict); - case "none" -> result.sameSite(SameSite.None); - default -> {} - } - } - String maxAge = cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.MAX_AGE_ATTR); - if (maxAge != null) { - result.maxAge(Long.parseLong(maxAge)); - } - result.domain(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.DOMAIN_ATTR)); - result.path(cookie.getAttribute(org.apache.hc.client5.http.cookie.Cookie.PATH_ATTR)); - result.httpOnly(cookie.isHttpOnly()); - result.secure(cookie.isSecure()); - return result; - } - /** * Headers implementation. * @@ -359,10 +335,11 @@ private static MutableConvertibleMultiValuesMap convertHeaders( ) { Map> map = new HashMap<>(); for (Header header: headers) { - if (!map.containsKey(header.getName())) { - map.put(header.getName(), new ArrayList<>(1)); + String name = standardizeHeader(header.getName()); + if (!map.containsKey(name)) { + map.put(name, new ArrayList<>(1)); } - map.get(header.getName()).add(header.getValue()); + map.get(name).add(header.getValue()); } return new MutableConvertibleMultiValuesMap<>(map, conversionService); } From 5213a917c44f73693ea43db60ece518811ea597c Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 16 Aug 2024 11:14:49 -0400 Subject: [PATCH 105/180] Fix the LimitingInputStream --- .../http/poja/util/LimitingInputStream.java | 78 ++++--------------- 1 file changed, 13 insertions(+), 65 deletions(-) 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 index 869121de8..47eb7bdaa 100644 --- 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 @@ -17,7 +17,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; /** * A wrapper around input stream that limits the maximum size to be read. @@ -33,13 +32,12 @@ public LimitingInputStream(InputStream stream, long maxSize) { this.stream = stream; } - @Override - public synchronized void mark(int readlimit) { - stream.mark(readlimit); - } - @Override public int read() throws IOException { + if (size >= maxSize) { + return -1; + } + ++size; return stream.read(); } @@ -49,9 +47,12 @@ public int read(byte[] b) throws IOException { 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 size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; + return sizeRead; } } @@ -61,40 +62,16 @@ public int read(byte[] b, int off, int len) throws IOException { if (size >= maxSize) { return -1; } - int sizeRead = stream.read(b, off, len); - size += sizeRead + off; - return size > maxSize ? sizeRead + (int) (maxSize - size) : sizeRead; + int lengthToRead = (int) Math.min(len, maxSize - size); + int sizeRead = stream.read(b, off, lengthToRead); + size += sizeRead; + return sizeRead; } } - @Override - public byte[] readAllBytes() throws IOException { - return stream.readNBytes((int) (maxSize - size)); - } - - @Override - public byte[] readNBytes(int len) throws IOException { - return stream.readNBytes(len); - } - - @Override - public int readNBytes(byte[] b, int off, int len) throws IOException { - return stream.readNBytes(b, off, len); - } - - @Override - public long skip(long n) throws IOException { - return stream.skip(n); - } - - @Override - public void skipNBytes(long n) throws IOException { - stream.skipNBytes(n); - } - @Override public int available() throws IOException { - return stream.available(); + return (int) (maxSize - size); } @Override @@ -102,33 +79,4 @@ public void close() throws IOException { stream.close(); } - @Override - public synchronized void reset() throws IOException { - stream.reset(); - } - - @Override - public boolean markSupported() { - return stream.markSupported(); - } - - @Override - public long transferTo(OutputStream out) throws IOException { - return stream.transferTo(out); - } - - @Override - public int hashCode() { - return stream.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return stream.equals(obj); - } - - @Override - public String toString() { - return stream.toString(); - } } From 0e25da7e2d02d7f86aff21a90297eb95b56c2c28 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 16 Aug 2024 13:08:41 -0400 Subject: [PATCH 106/180] Fix header standardization Fix header standardization method and refactor header and query parameters to separate classes --- .../poja/llhttp/ApacheServletHttpRequest.java | 199 +++--------------- .../http/poja/util/MultiValueHeaders.java | 109 ++++++++++ .../poja/util/MultiValuesQueryParameters.java | 76 +++++++ .../poja/BaseServerlessApplicationSpec.groovy | 104 --------- .../http/poja/SimpleServerSpec.groovy | 130 ------------ 5 files changed, 212 insertions(+), 406 deletions(-) create mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValueHeaders.java create mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValuesQueryParameters.java delete mode 100644 http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy delete mode 100644 http-poja-common/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index fa9989bcb..4f9381493 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -16,10 +16,7 @@ package io.micronaut.http.poja.llhttp; 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.HttpMethod; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpParameters; @@ -30,12 +27,15 @@ import io.micronaut.http.cookie.Cookies; import io.micronaut.http.poja.PojaHttpRequest; 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; @@ -46,14 +46,11 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; -import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; @@ -99,8 +96,8 @@ public ApacheServletHttpRequest( } catch (URISyntaxException e) { throw new RuntimeException("Could not get request URI", e); } - headers = new MultiValueHeaders(request.getHeaders(), conversionService); - queryParameters = new MultiValuesQueryParameters(uri, conversionService); + headers = createHeaders(request.getHeaders(), conversionService); + queryParameters = parseQueryParameters(uri, conversionService); cookies = parseCookies(request, conversionService); long contentLength = getContentLength(); @@ -118,6 +115,9 @@ public ApacheServletHttpRequest( } if (contentLength >= 0) { bodyStream = new LimitingInputStream(bodyStream, contentLength); + } else { + // Empty + bodyStream = new ByteArrayInputStream(new byte[0]); } byteBody = InputStreamByteBody.create( bodyStream, optionalContentLength, ioExecutor @@ -129,7 +129,7 @@ public ApacheServletHttpRequest( @Override public ClassicHttpRequest getNativeRequest() { - return null; + return request; } @Override @@ -174,6 +174,7 @@ public MutableHttpRequest body(T body) { } @Override + @SuppressWarnings("unchecked") public @NonNull Optional getBody() { return (Optional) getBody(Object.class); } @@ -271,172 +272,26 @@ private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService return cookies; } - /** - * Headers implementation. - * - * @param headers The values - */ - public record MultiValueHeaders( - MutableConvertibleMultiValuesMap headers - ) implements MutableHttpHeaders { - - public MultiValueHeaders(Header[] headers, ConversionService conversionService) { - this(convertHeaders(headers, conversionService)); - } - - 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 convertHeaders( - Header[] headers, ConversionService conversionService - ) { - Map> map = new HashMap<>(); - for (Header header: headers) { - String name = standardizeHeader(header.getName()); - if (!map.containsKey(name)) { - map.put(name, new ArrayList<>(1)); - } - map.get(name).add(header.getValue()); - } - return new MutableConvertibleMultiValuesMap<>(map, conversionService); - } - - private static MutableConvertibleMultiValuesMap standardizeHeaders( - Map> headers, ConversionService conversionService - ) { - MutableConvertibleMultiValuesMap map - = new MutableConvertibleMultiValuesMap<>(Collections.emptyMap(), 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); - } - result.append(c); - upperCase = c == '-'; + private static MultiValueHeaders createHeaders( + Header[] headers, ConversionService conversionService + ) { + Map> map = new HashMap<>(); + for (Header header: headers) { + if (!map.containsKey(header.getName())) { + map.put(header.getName(), new ArrayList<>(1)); } - return result.toString(); + map.get(header.getName()).add(header.getValue()); } + return new MultiValueHeaders(map, conversionService); } - /** - * Query parameters implementation. - * - * @param queryParams The values - */ - private record MultiValuesQueryParameters( - MutableConvertibleMultiValuesMap queryParams - ) implements MutableHttpParameters { - - private MultiValuesQueryParameters(URI uri, ConversionService conversionService) { - this(new MutableConvertibleMultiValuesMap<>(parseQueryParameters(uri), 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); - } - - public static Map> parseQueryParameters(URI uri) { - return new URIBuilder(uri).getQueryParams().stream() - .collect(Collectors.groupingBy( - nameValuePair -> nameValuePair.getName(), - Collectors.mapping(nameValuePair -> nameValuePair.getValue(), Collectors.toList()) - )); - } - + 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); } + } 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 new file mode 100644 index 000000000..e14fdfee1 --- /dev/null +++ b/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValueHeaders.java @@ -0,0 +1,109 @@ +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.Collections; +import java.util.HashMap; +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 HashMap<>(), 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 new file mode 100644 index 000000000..f6087355a --- /dev/null +++ b/http-poja-common/src/main/java/io/micronaut/http/poja/util/MultiValuesQueryParameters.java @@ -0,0 +1,76 @@ +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/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy deleted file mode 100644 index 39b0b159c..000000000 --- a/http-poja-common/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.ServerlessApplication -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 io.micronaut.http.poja.llhttp.ServerlessApplication} that creates 2 - * pipes to communicate with the server and simplifies reading and writing to them. - */ - @Singleton - @Replaces(ServerlessApplication.class) - static class TestingServerlessApplication extends ServerlessApplication { - - 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 - ServerlessApplication 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-common/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja-common/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy deleted file mode 100644 index 0825d8f75..000000000 --- a/http-poja-common/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy +++ /dev/null @@ -1,130 +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-Type: text/plain - Content-Length: 32 - - 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-Type: application/json - Content-Length: 148 - - {"_links":{"self":[{"href":"http://h/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".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-Type: text/plain - Content-Length: 13 - - 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-Type: text/plain - Content-Length: 15 - - 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" - } - - } - -} From ebdde39fdbde882982b29c5b88c785baf918c7b6 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 16 Aug 2024 13:40:07 -0400 Subject: [PATCH 107/180] Refactor, fix checkstyle and javadoc --- config/checkstyle/suppressions.xml | 2 +- .../llhttp/ApacheServerlessApplication.java | 2 +- .../poja/llhttp/ApacheServletHttpRequest.java | 118 ++++++++++-------- .../llhttp/ApacheServletHttpResponse.java | 7 +- .../micronaut/http/poja/PojaHttpRequest.java | 6 + .../micronaut/http/poja/PojaHttpResponse.java | 2 + .../poja/PojaHttpServerlessApplication.java | 3 + .../http/poja/util/LimitingInputStream.java | 6 + .../http/poja/util/MultiValueHeaders.java | 16 ++- .../poja/util/MultiValuesQueryParameters.java | 15 +++ .../http/poja/util/QueryStringDecoder.java | 8 +- 11 files changed, 123 insertions(+), 62 deletions(-) diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index f5e7b7d58..74ccd47ad 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -10,5 +10,5 @@ - + diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java index 1988caffd..f69e23ea6 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java @@ -16,7 +16,6 @@ package io.micronaut.http.poja.llhttp; import io.micronaut.context.ApplicationContext; -import io.micronaut.context.annotation.DefaultImplementation; import io.micronaut.core.convert.ConversionService; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.poja.PojaHttpServerlessApplication; @@ -41,6 +40,7 @@ * Implementation of {@link PojaHttpServerlessApplication} for Apache. * * @author Andriy Dmytruk. + * @since 4.10.0 */ @Singleton public class ApacheServerlessApplication diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index 4f9381493..7acfe4ae4 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -58,7 +58,8 @@ * An implementation of the POJA Http Request based on Apache. * * @param Body type - * @author Andriy Dmytruk. + * @author Andriy Dmytruk + * @since 4.10.0 */ public class ApacheServletHttpRequest extends PojaHttpRequest { @@ -72,6 +73,15 @@ public class ApacheServletHttpRequest extends PojaHttpRequest= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(); try { InputStream bodyStream = inputStream; - if (sessionInputBuffer.available() > 0) { - byte[] data = new byte[sessionInputBuffer.available()]; + if (sessionInputBuffer.length() > 0) { + byte[] data = new byte[sessionInputBuffer.length()]; sessionInputBuffer.read(data, inputStream); bodyStream = new CombinedInputStream( @@ -189,6 +199,57 @@ public void setConversionService(@NonNull ConversionService conversionService) { } + private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { + SimpleCookies cookies = new SimpleCookies(conversionService); + + // Manually parse cookies from the response headers + for (Header header : request.getHeaders(MultiValueHeaders.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 HashMap<>(); + 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}. @@ -243,55 +304,4 @@ public void close() throws IOException { } } - private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { - SimpleCookies cookies = new SimpleCookies(conversionService); - - // Manually parse cookies from the response headers - for (Header header : request.getHeaders(MultiValueHeaders.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 HashMap<>(); - 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); - } - } 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 index 33134ead8..d6d4f1dc6 100644 --- 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 @@ -31,7 +31,6 @@ 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 org.apache.hc.core5.http.message.BasicHttpResponse; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; @@ -45,6 +44,7 @@ * * @param The body type * @author Andriy Dmytruk + * @since 4.10.0 */ public class ApacheServletHttpResponse extends PojaHttpResponse { @@ -56,6 +56,11 @@ public class ApacheServletHttpResponse extends PojaHttpResponse 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); } 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 index 5fe8e9708..128117001 100644 --- 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 @@ -58,6 +58,7 @@ * @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 { @@ -134,6 +135,11 @@ public T consumeBody(Function consumer) { } } + /** + * 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 { 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 index 2338d4992..8a319987d 100644 --- 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 @@ -22,6 +22,8 @@ * * @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 index 28cde7e4c..7ca008fae 100644 --- 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 @@ -34,7 +34,10 @@ * 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 { 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 index 47eb7bdaa..c6a28ac04 100644 --- 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 @@ -27,6 +27,12 @@ public class LimitingInputStream extends InputStream { 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; 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 index e14fdfee1..b0187c157 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; @@ -8,7 +23,6 @@ import io.micronaut.http.MutableHttpHeaders; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; 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 index f6087355a..c17252e3b 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index bb2d2e716..8381441a8 100644 --- 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 @@ -32,7 +32,7 @@ * 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");
+ * {@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");
@@ -40,13 +40,13 @@
  * 
* * This decoder can also decode the content of an HTTP POST request whose - * content type is application/x-www-form-urlencoded: + * content type is application/x-www-form-urlencoded: *
- * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("recipient=world&x=1;y=2", false);
+ * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("recipient=world&x=1;y=2", false);
  * ...
  * 
* - *

HashDOS vulnerability fix

+ * 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 From 5c748e1167589c72a20e2f00ba8853064a202441 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 16 Aug 2024 15:02:25 -0400 Subject: [PATCH 108/180] Minor fix for sample --- .../io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java | 2 +- .../main/java/io/micronaut/http/poja/sample/TestController.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index 7acfe4ae4..0e145af3c 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -123,7 +123,7 @@ public ApacheServletHttpRequest( inputStream ); } - if (contentLength >= 0) { + if (contentLength > 0) { bodyStream = new LimitingInputStream(bodyStream, contentLength); } else { // Empty diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index b9dee602e..96d511c73 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -47,7 +47,6 @@ public final void delete() { @Post("/{name}") @Status(HttpStatus.CREATED) public final String create(@NonNull String name, HttpRequest request) { - request.getBody(); return "Hello, " + name + "\n"; } From faadb0e287ef35faf0134cc2f3d6e34e9c484c65 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 16 Aug 2024 15:16:26 -0400 Subject: [PATCH 109/180] Fix TCK client configuration --- .../http/poja/llhttp/ApacheServletHttpResponse.java | 1 - .../http/server/tck/poja/PojaApacheServerUnderTest.java | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 index d6d4f1dc6..4c6bfdd10 100644 --- 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 @@ -80,7 +80,6 @@ public ClassicHttpResponse getNativeResponse() { ByteArrayEntity body = new ByteArrayEntity(out.toByteArray(), contentType); response.setEntity(body); return response; - } @Override 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 index d4af5b18f..3f757e03b 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -60,8 +61,10 @@ public PojaApacheServerUnderTest(Map properties) { application.start(); port = application.getPort(); try { - client = HttpClient.create(new URL("http://localhost:" + port)) - .toBlocking(); + client = HttpClient.create( + new URL("http://localhost:" + port), + applicationContext.getBean(HttpClientConfiguration.class) + ).toBlocking(); } catch (MalformedURLException e) { throw new RuntimeException("Could not create HttpClient", e); } From 08d80066fdb4c92a81ea36af2cbd23b9ac582e1b Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 08:16:48 -0400 Subject: [PATCH 110/180] Enable CORS test --- .../http/server/tck/poja/PojaApacheServerTestSuite.java | 2 -- 1 file changed, 2 deletions(-) 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 index 25278b886..c9d90c7d2 100644 --- 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 @@ -33,8 +33,6 @@ "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", - // Cors are not supported and should be handled by a proxy - "io.micronaut.http.server.tck.tests.cors.CorsSimpleRequestTest", // Proxying is probably not supported. There is no request concurrency "io.micronaut.http.server.tck.tests.FilterProxyTest", }) From 543f642c4099b1ad828803c5f80a96e1387ba854 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 11:14:29 -0400 Subject: [PATCH 111/180] Add a test --- http-poja-common/build.gradle | 1 + .../poja/util/LimitingInputStreamSpec.groovy | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 http-poja-common/src/test/groovy/io/micronaut/http/poja/util/LimitingInputStreamSpec.groovy diff --git a/http-poja-common/build.gradle b/http-poja-common/build.gradle index bd8617cab..27f4afdd2 100644 --- a/http-poja-common/build.gradle +++ b/http-poja-common/build.gradle @@ -23,6 +23,7 @@ dependencies { compileOnly(mn.reactor) compileOnly(mn.micronaut.json.core) + testImplementation(mn.reactor) testImplementation(mnSerde.micronaut.serde.jackson) } 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 new file mode 100644 index 000000000..2d0bb8a72 --- /dev/null +++ b/http-poja-common/src/test/groovy/io/micronaut/http/poja/util/LimitingInputStreamSpec.groovy @@ -0,0 +1,39 @@ +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" + } + +} From 8cff2559bb6ed877426b5dd563b189c9cf5d686a Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 12:23:53 -0400 Subject: [PATCH 112/180] Remove sample and tck from being modules --- test-sample-poja/build.gradle | 15 ++++++++++----- .../build.gradle | 9 +-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index 52cdfd159..d1148b6b3 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -14,17 +14,24 @@ * limitations under the License. */ plugins { - id("io.micronaut.build.internal.servlet.module") + 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 { @@ -33,8 +40,6 @@ run { standardOutput = System.out } -micronautBuild { - binaryCompatibility { - enabled.set(false) - } +test { + useJUnitPlatform() } diff --git a/test-suite-http-server-tck-poja-apache/build.gradle b/test-suite-http-server-tck-poja-apache/build.gradle index 988fe30c8..3bfa83eeb 100644 --- a/test-suite-http-server-tck-poja-apache/build.gradle +++ b/test-suite-http-server-tck-poja-apache/build.gradle @@ -1,6 +1,5 @@ plugins { - id("io.micronaut.build.internal.servlet.module") - id("java-library") + id("io.micronaut.build.internal.servlet.http-server-tck-module") } dependencies { @@ -23,9 +22,3 @@ dependencies { testImplementation(mnSerde.micronaut.serde.jackson) testImplementation(mn.micronaut.http.client) } - -micronautBuild { - binaryCompatibility { - enabled.set(false) - } -} From 3141c0470a29daecbbe71b79a4094eb1dcbfe634 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 12:37:51 -0400 Subject: [PATCH 113/180] Create PojaHttpServerlessApplicationContextConfigurer --- ...rvlerlessApplicationContextConfigurer.java | 21 +++++++++++++++++++ .../http/poja/sample/Application.java | 10 ++------- 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServlerlessApplicationContextConfigurer.java 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 new file mode 100644 index 000000000..f2dcc79ee --- /dev/null +++ b/http-poja-common/src/main/java/io/micronaut/http/poja/PojaHttpServlerlessApplicationContextConfigurer.java @@ -0,0 +1,21 @@ +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/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 index ed172a79a..a37b56865 100644 --- 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 @@ -25,15 +25,9 @@ */ public class Application { - public static void main(String[] args) throws Exception { - // Need to disable banner because Micronaut prints banner to STDOUT, - // which gets mixed with HTTP response. - // See GCN-4489 - Micronaut.build(args) - .banner(false) - .mainClass(Application.class) - .start(); + public static void main(String[] args) { Micronaut.run(Application.class, args); } + } From 242bcc78631ef1e9682a0fa756d23ce45fe4f501 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 13:56:41 -0400 Subject: [PATCH 114/180] Create more specific exceptions Handle the case of a bad request where it could not be parsed. The server should not break, but return a 400 response. Also implement other review comments. --- .../llhttp/ApacheServerlessApplication.java | 33 ++++++++++++------- .../poja/llhttp/ApacheServletHttpRequest.java | 25 ++++++++------ .../llhttp/ApacheServletHttpResponse.java | 4 ++- .../ApacheServletBadRequestException.java | 23 +++++++++++++ .../http/poja/SimpleServerSpec.groovy | 17 ++++++++++ .../poja/PojaHttpServerlessApplication.java | 31 ++++++++--------- .../http/poja/util/MultiValueHeaders.java | 4 +-- 7 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java index f69e23ea6..1ac8fb1c6 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java @@ -17,8 +17,12 @@ 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; @@ -46,6 +50,10 @@ public class ApacheServerlessApplication extends PojaHttpServerlessApplication, ApacheServletHttpResponse> { + private final ConversionService conversionService; + private final MediaTypeCodecRegistry codecRegistry; + private final ExecutorService ioExecutor; + /** * Default constructor. * @@ -55,25 +63,28 @@ public class ApacheServerlessApplication 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)); } @Override protected void handleSingleRequest( ServletHttpHandler, ApacheServletHttpResponse> servletHttpHandler, - ApplicationContext applicationContext, InputStream in, OutputStream out ) throws IOException { - ConversionService conversionService = applicationContext.getConversionService(); - MediaTypeCodecRegistry codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); - ExecutorService ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); - ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); - ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, response - ); - - servletHttpHandler.service(exchange); + try { + ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( + in, conversionService, codecRegistry, ioExecutor, response + ); + 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); } @@ -83,7 +94,7 @@ private void writeResponse(ClassicHttpResponse response, OutputStream out) throw try { responseWriter.write(response, buffer, out); } catch (HttpException e) { - throw new RuntimeException("Could not write response body", e); + throw new HttpServerException("Could not write response body", e); } buffer.flush(out); diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index 0e145af3c..761e8f0d6 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -15,6 +15,7 @@ */ 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.HttpMethod; @@ -26,6 +27,7 @@ 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; @@ -47,6 +49,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -61,12 +64,13 @@ * @author Andriy Dmytruk * @since 4.10.0 */ -public class ApacheServletHttpRequest extends PojaHttpRequest { +@Internal +public final class ApacheServletHttpRequest extends PojaHttpRequest { private final ClassicHttpRequest request; private final HttpMethod method; - private final URI uri; + private URI uri; private final MultiValueHeaders headers; private final MultiValuesQueryParameters queryParameters; private final SimpleCookies cookies; @@ -97,14 +101,14 @@ public ApacheServletHttpRequest( try { request = parser.parse(sessionInputBuffer, inputStream); } catch (HttpException | IOException e) { - throw new RuntimeException("Could parse HTTP request", 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 RuntimeException("Could not get request URI", e); + throw new ApacheServletBadRequestException("Could not get request URI", e); } headers = createHeaders(request.getHeaders(), conversionService); queryParameters = parseQueryParameters(uri, conversionService); @@ -133,7 +137,7 @@ public ApacheServletHttpRequest( bodyStream, optionalContentLength, ioExecutor ); } catch (IOException e) { - throw new RuntimeException("Could not get request body", e); + throw new ApacheServletBadRequestException("Could not parse request body", e); } } @@ -170,12 +174,13 @@ public MutableHttpRequest cookie(Cookie cookie) { @Override public MutableHttpRequest uri(URI uri) { - return null; + this.uri = uri; + return this; } @Override public MutableHttpRequest body(T body) { - return null; + throw new UnsupportedOperationException("Could not change request body"); } @Override @@ -196,7 +201,7 @@ public MutableHttpRequest body(T body) { @Override public void setConversionService(@NonNull ConversionService conversionService) { - + // Not implemented } private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService conversionService) { @@ -231,7 +236,7 @@ private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService private static MultiValueHeaders createHeaders( Header[] headers, ConversionService conversionService ) { - Map> map = new HashMap<>(); + Map> map = new LinkedHashMap<>(); for (Header header: headers) { if (!map.containsKey(header.getName())) { map.put(header.getName(), new ArrayList<>(1)); @@ -254,7 +259,7 @@ private static MultiValuesQueryParameters parseQueryParameters(URI uri, Conversi * 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 class CombinedInputStream extends InputStream { + private final static class CombinedInputStream extends InputStream { private final InputStream first; private final InputStream second; 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 index 4c6bfdd10..3f837cc9b 100644 --- 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 @@ -15,6 +15,7 @@ */ 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; @@ -46,7 +47,8 @@ * @author Andriy Dmytruk * @since 4.10.0 */ -public class ApacheServletHttpResponse extends PojaHttpResponse { +@Internal +public final class ApacheServletHttpResponse extends PojaHttpResponse { private int code = HttpStatus.OK.getCode(); private String reasonPhrase = HttpStatus.OK.getReason(); 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 new file mode 100644 index 000000000..c0203b2c3 --- /dev/null +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java @@ -0,0 +1,23 @@ +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/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy index 36ddd3c07..34e82bc8b 100644 --- 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 @@ -45,6 +45,23 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { {"_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-Type: text/plain + Content-Length: 32 + + HTTP request could not be parsed""".stripIndent() + } + void "test DELETE method"() { when: app.write("""\ 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 index 7ca008fae..ed8c23902 100644 --- 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 @@ -39,7 +39,7 @@ * @author Andriy Dmytruk. * @since 4.10.0 */ -public abstract class PojaHttpServerlessApplication implements EmbeddedApplication { +public abstract class PojaHttpServerlessApplication implements EmbeddedApplication> { private final ApplicationContext applicationContext; private final ApplicationConfiguration applicationConfiguration; @@ -78,7 +78,7 @@ public boolean isRunning() { * @param output The output stream * @return The application */ - public @NonNull PojaHttpServerlessApplication start(InputStream input, OutputStream output) { + public @NonNull PojaHttpServerlessApplication start(InputStream input, OutputStream output) { final ServletHttpHandler servletHttpHandler = new ServletHttpHandler<>(applicationContext, null) { @Override @@ -87,7 +87,7 @@ protected ServletExchange createExchange(Object request, Object respon } }; try { - runIndefinitely(servletHttpHandler, applicationContext, input, output); + runIndefinitely(servletHttpHandler, input, output); } catch (IOException e) { throw new RuntimeException(e); } @@ -95,7 +95,7 @@ protected ServletExchange createExchange(Object request, Object respon } @Override - public @NonNull PojaHttpServerlessApplication start() { + public @NonNull PojaHttpServerlessApplication start() { try { // Default streams to streams based on System.inheritedChannel. // If not possible, use System.in/out. @@ -117,17 +117,18 @@ protected ServletExchange createExchange(Object request, Object respon * A method to start the application in a loop. * * @param servletHttpHandler The handler - * @param applicationContext The application context * @param in The input stream * @param out The output stream * @throws IOException IO exception */ - protected void runIndefinitely(ServletHttpHandler servletHttpHandler, - ApplicationContext applicationContext, - InputStream in, - OutputStream out) throws IOException { + @SuppressWarnings("InfiniteLoopStatement") + protected void runIndefinitely( + ServletHttpHandler servletHttpHandler, + InputStream in, + OutputStream out + ) throws IOException { while (true) { - handleSingleRequest(servletHttpHandler, applicationContext, in, out); + handleSingleRequest(servletHttpHandler, in, out); } } @@ -135,15 +136,15 @@ protected void runIndefinitely(ServletHttpHandler servletHttpHandler, * Handle a single request. * * @param servletHttpHandler The handler - * @param applicationContext The application context * @param in The input stream * @param out The output stream * @throws IOException IO exception */ - protected abstract void handleSingleRequest(ServletHttpHandler servletHttpHandler, - ApplicationContext applicationContext, - InputStream in, - OutputStream out) throws IOException; + protected abstract void handleSingleRequest( + ServletHttpHandler servletHttpHandler, + InputStream in, + OutputStream out + ) throws IOException; @Override public @NonNull PojaHttpServerlessApplication stop() { 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 index b0187c157..7e6d8cc02 100644 --- 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 @@ -23,7 +23,7 @@ import io.micronaut.http.MutableHttpHeaders; import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -89,7 +89,7 @@ private static MutableConvertibleMultiValuesMap standardizeHeaders( Map> headers, ConversionService conversionService ) { MutableConvertibleMultiValuesMap map - = new MutableConvertibleMultiValuesMap<>(new HashMap<>(), conversionService); + = new MutableConvertibleMultiValuesMap<>(new LinkedHashMap<>(), conversionService); for (String key: headers.keySet()) { map.put(standardizeHeader(key), headers.get(key)); } From f298fa0f43b5b38f91556a658cd96ae46355a798 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 14:12:14 -0400 Subject: [PATCH 115/180] Add buffer size configuration options --- .../llhttp/ApacheServerlessApplication.java | 6 ++- .../llhttp/ApacheServletConfiguration.java | 39 +++++++++++++++++++ .../poja/llhttp/ApacheServletHttpRequest.java | 10 +++-- .../ApacheServletBadRequestException.java | 15 +++++++ ...rvlerlessApplicationContextConfigurer.java | 15 +++++++ 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java index 1ac8fb1c6..4afaf6a8b 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java @@ -53,6 +53,7 @@ public class ApacheServerlessApplication private final ConversionService conversionService; private final MediaTypeCodecRegistry codecRegistry; private final ExecutorService ioExecutor; + private final ApacheServletConfiguration configuration; /** * Default constructor. @@ -66,6 +67,7 @@ public ApacheServerlessApplication(ApplicationContext applicationContext, conversionService = applicationContext.getConversionService(); codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); + configuration = applicationContext.getBean(ApacheServletConfiguration.class); } @Override @@ -77,7 +79,7 @@ protected void handleSingleRequest( ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); try { ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, response + in, conversionService, codecRegistry, ioExecutor, response, configuration ); servletHttpHandler.service(exchange); } catch (ApacheServletBadRequestException e) { @@ -89,7 +91,7 @@ protected void handleSingleRequest( } private void writeResponse(ClassicHttpResponse response, OutputStream out) throws IOException { - SessionOutputBuffer buffer = new SessionOutputBufferImpl(8 * 1024); + SessionOutputBuffer buffer = new SessionOutputBufferImpl(configuration.outputBufferSize()); DefaultHttpResponseWriter responseWriter = new DefaultHttpResponseWriter(); try { responseWriter.write(response, buffer, out); 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 new file mode 100644 index 000000000..7740bde64 --- /dev/null +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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 index 761e8f0d6..0789c75c1 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -48,7 +48,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -85,17 +84,20 @@ public final class ApacheServletHttpRequest extends PojaHttpRequest response + ApacheServletHttpResponse response, + ApacheServletConfiguration configuration ) { super(conversionService, codecRegistry, response); - SessionInputBufferImpl sessionInputBuffer = new SessionInputBufferImpl(8192); + SessionInputBufferImpl sessionInputBuffer + = new SessionInputBufferImpl(configuration.inputBufferSize()); DefaultHttpRequestParser parser = new DefaultHttpRequestParser(); try { @@ -259,7 +261,7 @@ private static MultiValuesQueryParameters parseQueryParameters(URI uri, Conversi * 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 final static class CombinedInputStream extends InputStream { + private static final class CombinedInputStream extends InputStream { private final InputStream first; private final InputStream second; 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 index c0203b2c3..3efb664eb 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index f2dcc79ee..78eb60d77 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; From 63abdc53c62f4945b01dc05eedf3c4414b313b7f Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 14:30:05 -0400 Subject: [PATCH 116/180] Fix sonar cloud warnings --- .../poja/llhttp/ApacheServletHttpRequest.java | 3 +- .../micronaut/http/poja/PojaBodyBinder.java | 54 +++++++++++-------- .../micronaut/http/poja/PojaHttpRequest.java | 2 +- .../poja/PojaHttpServerlessApplication.java | 2 +- .../TestingServerlessEmbeddedApplication.java | 5 +- .../tck/poja/PojaApacheServerUnderTest.java | 1 + 6 files changed, 40 insertions(+), 27 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index 0789c75c1..d4683758e 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -18,6 +18,7 @@ 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; @@ -210,7 +211,7 @@ private SimpleCookies parseCookies(ClassicHttpRequest request, ConversionService SimpleCookies cookies = new SimpleCookies(conversionService); // Manually parse cookies from the response headers - for (Header header : request.getHeaders(MultiValueHeaders.COOKIE)) { + for (Header header : request.getHeaders(HttpHeaders.COOKIE)) { String cookie = header.getValue(); String name = null; 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 index cd9b958c1..6546ee9a1 100644 --- 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 @@ -82,28 +82,9 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest pojaHttpRequest) { if (CharSequence.class.isAssignableFrom(type) && name == null) { - 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) Optional.of(content); - } catch (IOException e) { - LOG.debug("Error occurred reading function body: {}", e.getMessage(), e); - return new ConversionFailedBindingResult<>(e); - } - }); + return (BindingResult) bindCharSequence(pojaHttpRequest, source); } else if (argument.getType().isAssignableFrom(byte[].class) && name == null) { - return pojaHttpRequest.consumeBody(inputStream -> { - try { - byte[] bytes = inputStream.readAllBytes(); - return () -> (Optional) Optional.of(bytes); - } catch (IOException e) { - LOG.debug("Error occurred reading function body: {}", e.getMessage(), e); - return new ConversionFailedBindingResult<>(e); - } - }); + return (BindingResult) bindByteArray(pojaHttpRequest); } else { final MediaType mediaType = source.getContentType().orElse(MediaType.APPLICATION_JSON_TYPE); if (pojaHttpRequest.isFormSubmission()) { @@ -136,6 +117,33 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest 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 ) { @@ -187,7 +195,7 @@ private BindingResult bindPublisher( if (Publishers.isSingle(type)) { T content = (T) codec.decode(typeArg, inputStream); final Publisher publisher = Publishers.just(content); - LOG.trace("Decoded object from function body: {}", content); + LOG.trace("Decoded single publisher from function body: {}", content); final T converted = conversionService.convertRequired(publisher, type); return () -> Optional.of(converted); } else { @@ -218,7 +226,7 @@ private BindingResult bindPublisher( } } T content = (T) codec.decode(containerType, inputStream); - LOG.trace("Decoded object from function body: {}", content); + 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); 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 index 128117001..4c5694878 100644 --- 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 @@ -140,7 +140,7 @@ public T consumeBody(Function consumer) { * * @return The form data as multi-values. */ - protected ConvertibleMultiValues getFormData() { + protected ConvertibleMultiValues getFormData() { return consumeBody(inputStream -> { try { String content = IOUtils.readText(new BufferedReader(new InputStreamReader( 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 index ed8c23902..cb14fdc02 100644 --- 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 @@ -121,7 +121,7 @@ protected ServletExchange createExchange(Object request, Object respon * @param out The output stream * @throws IOException IO exception */ - @SuppressWarnings("InfiniteLoopStatement") + @SuppressWarnings({"InfiniteLoopStatement", "java:S2189"}) protected void runIndefinitely( ServletHttpHandler servletHttpHandler, InputStream in, 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 index 72c0cb3e0..f9654c3da 100644 --- 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 @@ -42,6 +42,7 @@ 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; @@ -75,6 +76,8 @@ public class TestingServerlessEmbeddedApplication implements EmbeddedServer { private Pipe outputPipe; private Thread serverThread; + private final static SecureRandom random = new SecureRandom(); + /** * Default constructor. * @@ -89,7 +92,7 @@ public TestingServerlessEmbeddedApplication( private void createServerSocket() { IOException exception = null; for (int i = 0; i < 100; ++i) { - port = new Random().nextInt(10000, 20000); + port = random.nextInt(10000, 20000); try { serverSocket = new ServerSocket(port); return; 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 index 3f757e03b..dfc5e339d 100644 --- 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 @@ -35,6 +35,7 @@ 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); From ed1fdb24fa5c38bc587d63e6bbb557186249bf74 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 14:31:47 -0400 Subject: [PATCH 117/180] Minor style fix --- .../poja/test/TestingServerlessEmbeddedApplication.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index f9654c3da..9889bdce2 100644 --- 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 @@ -46,7 +46,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -64,6 +63,8 @@ @Replaces(EmbeddedApplication.class) public class TestingServerlessEmbeddedApplication implements EmbeddedServer { + private static final SecureRandom RANDOM = new SecureRandom(); + private PojaHttpServerlessApplication application; private AtomicBoolean isRunning = new AtomicBoolean(false); @@ -76,8 +77,6 @@ public class TestingServerlessEmbeddedApplication implements EmbeddedServer { private Pipe outputPipe; private Thread serverThread; - private final static SecureRandom random = new SecureRandom(); - /** * Default constructor. * @@ -92,7 +91,7 @@ public TestingServerlessEmbeddedApplication( private void createServerSocket() { IOException exception = null; for (int i = 0; i < 100; ++i) { - port = random.nextInt(10000, 20000); + port = RANDOM.nextInt(10000, 20000); try { serverSocket = new ServerSocket(port); return; From 5c586f8540e5734e925374f383eac8a618bf7f53 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 14:44:11 -0400 Subject: [PATCH 118/180] More sonar cloud fixes --- .../micronaut/http/poja/PojaBodyBinder.java | 36 +++++++++++-------- .../http/poja/util/QueryStringDecoder.java | 1 + .../TestingServerlessEmbeddedApplication.java | 1 + 3 files changed, 23 insertions(+), 15 deletions(-) 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 index 6546ee9a1..eadd92a25 100644 --- 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 @@ -80,6 +80,7 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest 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); @@ -94,29 +95,34 @@ public BindingResult bind(ArgumentConversionContext context, HttpRequest { - 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); - } - }); + 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 { 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 index 8381441a8..1564e6d3a 100644 --- 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 @@ -59,6 +59,7 @@ * . *

*/ +@SuppressWarnings("java:S3776" /* Reduce cognitive complexity warning */) public class QueryStringDecoder { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 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 index 9889bdce2..d71fc3f4e 100644 --- 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 @@ -209,6 +209,7 @@ 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)); From da74733b3d7e295d8e048c90c2bf64b3a3af2b47 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 15:15:33 -0400 Subject: [PATCH 119/180] Fix TCK test with native-image --- .../resources/META-INF/native-image/resource-config.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/resource-config.json diff --git a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/resource-config.json b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/resource-config.json new file mode 100644 index 000000000..d121a0297 --- /dev/null +++ b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources": { + "includes": [ + {"pattern": "assets/hello.txt"} + ] + } +} From 330b5f6f30ec10dd85299b656e9a8b44dc0ead99 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 16:04:57 -0400 Subject: [PATCH 120/180] Attempt to fix GraalVM 17 native image --- .../http/server/tck/poja/PojaApacheServerUnderTest.java | 1 + 1 file changed, 1 insertion(+) 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 index dfc5e339d..f456d214d 100644 --- 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 @@ -50,6 +50,7 @@ public PojaApacheServerUnderTest(Map properties) { properties.put("endpoints.health.service-ready-indicator-enabled", StringUtils.FALSE); properties.put("endpoints.refresh.enabled", StringUtils.FALSE); properties.put("micronaut.security.enabled", StringUtils.FALSE); + properties.put("micronaut.http.client.read-timeout", "30s"); applicationContext = ApplicationContext .builder(Environment.FUNCTION, Environment.TEST) .eagerInitConfiguration(true) From 5b1a41c25a251bf60502b864d004b01465fcee4f Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 19 Aug 2024 17:25:37 -0400 Subject: [PATCH 121/180] Revert and fix GraalVM TCK by adding required configurations --- .../TestingServerlessEmbeddedApplication.java | 2 ++ .../build.gradle | 15 ---------- .../tck/poja/PojaApacheServerUnderTest.java | 1 - .../reflect-config.json | 29 +++++++++++++++++++ .../resource-config.json | 8 +++++ .../native-image/resource-config.json | 7 ----- 6 files changed, 39 insertions(+), 23 deletions(-) create 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 create 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/native-image/resource-config.json 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 index d71fc3f4e..90cc876c0 100644 --- 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 @@ -141,9 +141,11 @@ public TestingServerlessEmbeddedApplication start() { 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) { diff --git a/test-suite-http-server-tck-poja-apache/build.gradle b/test-suite-http-server-tck-poja-apache/build.gradle index 3bfa83eeb..d8050432a 100644 --- a/test-suite-http-server-tck-poja-apache/build.gradle +++ b/test-suite-http-server-tck-poja-apache/build.gradle @@ -3,22 +3,7 @@ plugins { } dependencies { - testAnnotationProcessor(platform(mn.micronaut.core.bom)) - testAnnotationProcessor(mn.micronaut.inject.java) - testImplementation(platform(mn.micronaut.core.bom)) - testImplementation(mn.micronaut.inject.java) - - testImplementation(mn.micronaut.http.client) - testImplementation(mn.micronaut.http.server.tck) - testImplementation(libs.junit.platform.engine) - testImplementation(mn.micronaut.jackson.databind) - testImplementation(libs.junit.jupiter.engine) - testRuntimeOnly(mnLogging.logback.classic) - testRuntimeOnly(mnValidation.micronaut.validation) - testImplementation(projects.micronautHttpPojaApache) testImplementation(projects.micronautHttpPojaTest) - testImplementation(mnSerde.micronaut.serde.jackson) - testImplementation(mn.micronaut.http.client) } 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 index f456d214d..dfc5e339d 100644 --- 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 @@ -50,7 +50,6 @@ public PojaApacheServerUnderTest(Map properties) { properties.put("endpoints.health.service-ready-indicator-enabled", StringUtils.FALSE); properties.put("endpoints.refresh.enabled", StringUtils.FALSE); properties.put("micronaut.security.enabled", StringUtils.FALSE); - properties.put("micronaut.http.client.read-timeout", "30s"); applicationContext = ApplicationContext .builder(Environment.FUNCTION, Environment.TEST) .eagerInitConfiguration(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/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 new file mode 100644 index 000000000..e565aba42 --- /dev/null +++ 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 @@ -0,0 +1,29 @@ +[ + { + "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 new file mode 100644 index 000000000..ae9383d82 --- /dev/null +++ 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 @@ -0,0 +1,8 @@ +{ + "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/native-image/resource-config.json b/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/resource-config.json deleted file mode 100644 index d121a0297..000000000 --- a/test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/native-image/resource-config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "resources": { - "includes": [ - {"pattern": "assets/hello.txt"} - ] - } -} From b6e7e33eef900932415f730050eef64b3dc3947f Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 20 Aug 2024 10:30:03 -0400 Subject: [PATCH 122/180] Fixes after Micronaut update --- .../io/micronaut/http/poja/SimpleServerSpec.groovy | 10 +++++----- .../io/micronaut/servlet/http/ServletHttpHandler.java | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) 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 index 34e82bc8b..b2da6492b 100644 --- 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 @@ -21,8 +21,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { then: app.read() == """\ HTTP/1.1 200 Ok - Content-Type: text/plain Content-Length: 32 + Content-Type: text/plain Hello, Micronaut Without Netty! """.stripIndent() @@ -39,8 +39,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { then: app.read() == """\ HTTP/1.1 404 Not Found - Content-Type: application/json Content-Length: 140 + Content-Type: application/json {"_links":{"self":[{"href":"/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""".stripIndent() } @@ -56,8 +56,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { then: app.read() == """\ HTTP/1.1 400 Bad Request - Content-Type: text/plain Content-Length: 32 + Content-Type: text/plain HTTP request could not be parsed""".stripIndent() } @@ -89,8 +89,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { then: app.read() == """\ HTTP/1.1 201 Created - Content-Type: text/plain Content-Length: 13 + Content-Type: text/plain Hello, Dream """.stripIndent() @@ -107,8 +107,8 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { then: app.read() == """\ HTTP/1.1 200 Ok - Content-Type: text/plain Content-Length: 15 + Content-Type: text/plain Hello, Dream1! """.stripIndent() diff --git a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java index e9c2d714d..ff1bdd5b7 100644 --- a/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java +++ b/servlet-core/src/main/java/io/micronaut/servlet/http/ServletHttpHandler.java @@ -46,6 +46,7 @@ import io.micronaut.http.context.event.HttpRequestReceivedEvent; import io.micronaut.http.context.event.HttpRequestTerminatedEvent; import io.micronaut.http.exceptions.HttpStatusException; +import io.micronaut.http.hateoas.JsonError; import io.micronaut.http.server.RequestLifecycle; import io.micronaut.http.server.RouteExecutor; import io.micronaut.http.server.types.files.FileCustomizableResponseType; @@ -420,7 +421,10 @@ private void encodeResponse(ServletExchange exchange, MessageBodyWriter messageBodyWriter = null; if (!(body instanceof HttpStatus)) { messageBodyWriter = routeInfoAttribute.map(RouteInfo::getMessageBodyWriter).orElse(null); - if (messageBodyWriter == null) { + if (messageBodyWriter == null + // A special case because if an exception is thrown the message content type might change + || (JsonError.class.isAssignableFrom(bodyType)) + ) { MediaType finalMediaType = mediaType; Argument finalBodyArgument = bodyArgument; Optional> writer = messageBodyHandlerRegistry.findWriter(bodyArgument, List.of(mediaType)); From 3fb0fab82d3a8a84b45662e59ef62b73fb96fc7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:39:14 -0400 Subject: [PATCH 123/180] Update dependency org.apache.tomcat.embed:tomcat-embed-core to v10.1.28 (#777) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a25df2299..1eb31cbb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' undertow = '2.3.15.Final' -tomcat = '10.1.26' +tomcat = '10.1.28' bcpkix = "1.70" managed-apache-http-core5 = "5.2.5" From 431186a051efc483ac2f2a7e20483b60af29b70c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:56:46 +0200 Subject: [PATCH 124/180] Update dependency gradle to v8.10 (#781) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 43504 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197d7c4586c843d1d3e9090525f1898cde..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 3990 zcmV;H4{7l5(*nQL0Kr1kzC=_KMxQY0|W5(lc#i zH*M1^P4B}|{x<+fkObwl)u#`$GxKKV&3pg*-y6R6txw)0qU|Clf9Uds3x{_-**c=7 z&*)~RHPM>Rw#Hi1R({;bX|7?J@w}DMF>dQQU2}9yj%iLjJ*KD6IEB2^n#gK7M~}6R zkH+)bc--JU^pV~7W=3{E*4|ZFpDpBa7;wh4_%;?XM-5ZgZNnVJ=vm!%a2CdQb?oTa z70>8rTb~M$5Tp!Se+4_OKWOB1LF+7gv~$$fGC95ToUM(I>vrd$>9|@h=O?eARj0MH zT4zo(M>`LWoYvE>pXvqG=d96D-4?VySz~=tPVNyD$XMshoTX(1ZLB5OU!I2OI{kb) zS8$B8Qm>wLT6diNnyJZC?yp{Kn67S{TCOt-!OonOK7$K)e-13U9GlnQXPAb&SJ0#3 z+vs~+4Qovv(%i8g$I#FCpCG^C4DdyQw3phJ(f#y*pvNDQCRZ~MvW<}fUs~PL=4??j zmhPyg<*I4RbTz|NHFE-DC7lf2=}-sGkE5e!RM%3ohM7_I^IF=?O{m*uUPH(V?gqyc(Rp?-Qu(3bBIL4Fz(v?=_Sh?LbK{nqZMD>#9D_hNhaV$0ef3@9V90|0u#|PUNTO>$F=qRhg1duaE z0`v~X3G{8RVT@kOa-pU+z8{JWyP6GF*u2e8eKr7a2t1fuqQy)@d|Qn(%YLZ62TWtoX@$nL}9?atE#Yw`rd(>cr0gY;dT9~^oL;u)zgHUvxc2I*b&ZkGM-iq=&(?kyO(3}=P! zRp=rErEyMT5UE9GjPHZ#T<`cnD)jyIL!8P{H@IU#`e8cAG5jMK zVyKw7--dAC;?-qEu*rMr$5@y535qZ6p(R#+fLA_)G~!wnT~~)|s`}&fA(s6xXN`9j zP#Fd3GBa#HeS{5&8p?%DKUyN^X9cYUc6vq}D_3xJ&d@=6j(6BZKPl?!k1?!`f3z&a zR4ZF60Mx7oBxLSxGuzA*Dy5n-d2K=+)6VMZh_0KetK|{e;E{8NJJ!)=_E~1uu=A=r zrn&gh)h*SFhsQJo!f+wKMIE;-EOaMSMB@aXRU(UcnJhZW^B^mgs|M9@5WF@s6B0p& zm#CTz)yiQCgURE{%hjxHcJ6G&>G9i`7MyftL!QQd5 z@RflRs?7)99?X`kHNt>W3l7YqscBpi*R2+fsgABor>KVOu(i(`03aytf2UA!&SC9v z!E}whj#^9~=XHMinFZ;6UOJjo=mmNaWkv~nC=qH9$s-8roGeyaW-E~SzZ3Gg>j zZ8}<320rg4=$`M0nxN!w(PtHUjeeU?MvYgWKZ6kkzABK;vMN0|U;X9abJleJA(xy<}5h5P(5 z{RzAFPvMnX2m0yH0Jn2Uo-p`daE|(O`YQiC#jB8;6bVIUf?SY(k$#C0`d6qT`>Xe0+0}Oj0=F&*D;PVe=Z<=0AGI<6$gYLwa#r` zm449x*fU;_+J>Mz!wa;T-wldoBB%&OEMJgtm#oaI60TSYCy7;+$5?q!zi5K`u66Wq zvg)Fx$s`V3Em{=OEY{3lmh_7|08ykS&U9w!kp@Ctuzqe1JFOGz6%i5}Kmm9>^=gih z?kRxqLA<3@e=}G4R_?phW{4DVr?`tPfyZSN@R=^;P;?!2bh~F1I|fB7P=V=9a6XU5 z<#0f>RS0O&rhc&nTRFOW7&QhevP0#>j0eq<1@D5yAlgMl5n&O9X|Vq}%RX}iNyRFF z7sX&u#6?E~bm~N|z&YikXC=I0E*8Z$v7PtWfjy)$e_Ez25fnR1Q=q1`;U!~U>|&YS zaOS8y!^ORmr2L4ik!IYR8@Dcx8MTC=(b4P6iE5CnrbI~7j7DmM8em$!da&D!6Xu)!vKPdLG z9f#)se|6=5yOCe)N6xDhPI!m81*dNe7u985zi%IVfOfJh69+#ag4ELzGne?o`eA`42K4T)h3S+s)5IT97%O>du- z0U54L8m4}rkRQ?QBfJ%DLssy^+a7Ajw;0&`NOTY4o;0-ivm9 zBz1C%nr_hQ)X)^QM6T1?=yeLkuG9Lf50(eH}`tFye;01&(p?8i+6h};VV-2B~qdxeC#=X z(JLlzy&fHkyi9Ksbcs~&r^%lh^2COldLz^H@X!s~mr9Dr6z!j+4?zkD@Ls7F8(t(f z9`U?P$Lmn*Y{K}aR4N&1N=?xtQ1%jqf1~pJyQ4SgBrEtR`j4lQuh7cqP49Em5cO=I zB(He2`iPN5M=Y0}h(IU$37ANTGx&|b-u1BYA*#dE(L-lptoOpo&th~E)_)y-`6kSH z3vvyVrcBwW^_XYReJ=JYd9OBQrzv;f2AQdZH#$Y{Y+Oa33M70XFI((fs;mB4e`<<{ ze4dv2B0V_?Ytsi>>g%qs*}oDGd5d(RNZ*6?7qNbdp7wP4T72=F&r?Ud#kZr8Ze5tB z_oNb7{G+(o2ajL$!69FW@jjPQ2a5C)m!MKKRirC$_VYIuVQCpf9rIms0GRDf)8AH${I`q^~5rjot@#3$2#zT2f`(N^P7Z;6(@EK$q*Jgif00I6*^ZGV+XB5uw*1R-@23yTw&WKD{s1;HTL;dO)%5i#`dc6b7;5@^{KU%N|A-$zsYw4)7LA{3`Zp>1 z-?K9_IE&z)dayUM)wd8K^29m-l$lFhi$zj0l!u~4;VGR6Y!?MAfBC^?QD53hy6VdD z@eUZIui}~L%#SmajaRq1J|#> z4m=o$vZ*34=ZWK2!QMNEcp2Lbc5N1q!lEDq(bz0b;WI9;e>l=CG9^n#ro`w>_0F$Q zfZ={2QyTkfByC&gy;x!r*NyXXbk=a%~~(#K?< zTke0HuF5{Q+~?@!KDXR|g+43$+;ab`^flS%miup_0OUTm=nIc%d5nLP)i308PIjl_YMF6cpQ__6&$n6it8K- z8PIjl_YMF6cpQ_!r)L8IivW`WdK8mBs6PXdjR2DYdK8nCs73=4j{uVadK8oNjwX|E wpAeHLsTu^*Y>Trk?aBtSQ(D-o$(D8Px^?ZI-PUB? z*1fv!{YdHme3Fc8%cR@*@zc5A_nq&2=R47Hp@$-JF4Fz*;SLw5}K^y>s-s;V!}b2i=5=M- zComP?ju>8Fe@=H@rlwe1l`J*6BTTo`9b$zjQ@HxrAhp0D#u?M~TxGC_!?ccCHCjt| zF*PgJf@kJB`|Ml}cmsyrAjO#Kjr^E5p29w+#>$C`Q|54BoDv$fQ9D?3n32P9LPMIzu?LjNqggOH=1@T{9bMn*u8(GI z!;MLTtFPHal^S>VcJdiYqX0VU|Rn@A}C1xOlxCribxes0~+n2 z6qDaIA2$?e`opx3_KW!rAgbpzU)gFdjAKXh|5w``#F0R|c)Y)Du0_Ihhz^S?k^pk% zP>9|pIDx)xHH^_~+aA=^$M!<8K~Hy(71nJGf6`HnjtS=4X4=Hk^O71oNia2V{HUCC zoN3RSBS?mZCLw;l4W4a+D8qc)XJS`pUJ5X-f^1ytxwr`@si$lAE?{4G|o; zO0l>`rr?;~c;{ZEFJ!!3=7=FdGJ?Q^xfNQh4A?i;IJ4}B+A?4olTK(fN++3CRBP97 ze~lG9h%oegkn)lpW-4F8o2`*WW0mZHwHez`ko@>U1_;EC_6ig|Drn@=DMV9YEUSCa zIf$kHei3(u#zm9I!Jf(4t`Vm1lltJ&lVHy(eIXE8sy9sUpmz%I_gA#8x^Zv8%w?r2 z{GdkX1SkzRIr>prRK@rqn9j2wG|rUvf6PJbbin=yy-TAXrguvzN8jL$hUrIXzr^s5 zVM?H4;eM-QeRFr06@ifV(ocvk?_)~N@1c2ien56UjWXid6W%6ievIh)>dk|rIs##^kY67ib8Kw%#-oVFaXG7$ERyA9(NSJUvWiOA5H(!{uOpcW zg&-?iqPhds%3%tFspHDqqr;A!e@B#iPQjHd=c>N1LoOEGRehVoPOdxJ>b6>yc#o#+ zl8s8!(|NMeqjsy@0x{8^j0d00SqRZjp{Kj)&4UHYGxG+z9b-)72I*&J70?+8e?p_@ z=>-(>l6z5vYlP~<2%DU02b!mA{7mS)NS_eLe=t)sm&+Pmk?asOEKlkPQ)EUvvfC=;4M&*|I!w}(@V_)eUKLA_t^%`o z0PM9LV|UKTLnk|?M3u!|f2S0?UqZsEIH9*NJS-8lzu;A6-rr-ot=dg9SASoluZUkFH$7X; zP=?kYX!K?JL-b~<#7wU;b;eS)O;@?h%sPPk{4xEBxb{!sm0AY|f9cNvx6>$3F!*0c z75H=dy8JvTyO8}g1w{$9T$p~5en}AeSLoCF>_RT9YPMpChUjl310o*$QocjbH& zbnwg#gssR#jDVN{uEi3n(PZ%PFZ|6J2 z5_rBf0-u>e4sFe0*Km49ATi7>Kn0f9!uc|rRMR1Dtt6m1LW8^>qFlo}h$@br=Rmpi z;mI&>OF64Be{dVeHI8utrh)v^wsZ0jii%x8UgZ8TC%K~@I(4E};GFW&(;WVov}3%H zH;IhRkfD^(vt^DjZz(MyHLZxv8}qzPc(%itBkBwf_fC~sDBgh<3XAv5cxxfF3<2U! z03Xe&z`is!JDHbe;mNmfkH+_LFE*I2^mdL@7(@9DfAcP6O04V-ko;Rpgp<%Cj5r8Z zd0`sXoIjV$j)--;jA6Zy^D5&5v$o^>e%>Q?9GLm{i~p^lAn!%ZtF$I~>39XVZxk0b zROh^Bk9cE0AJBLozZIEmy7xG(yHWGztvfnr0(2ro1%>zsGMS^EMu+S$r=_;9 zWwZkgf7Q7`H9sLf2Go^Xy6&h~a&%s2_T@_Csf19MntF$aVFiFkvE3_hUg(B@&Xw@YJ zpL$wNYf78=0c@!QU6_a$>CPiXT7QAGDM}7Z(0z#_ZA=fmLUj{2z7@Ypo71UDy8GHr z-&TLKf6a5WCf@Adle3VglBt4>Z>;xF}}-S~B7<(%B;Y z0QR55{z-buw>8ilNM3u6I+D$S%?)(p>=eBx-HpvZj{7c*_?K=d()*7q?93us}1dq%FAFYLsW8ZTQ_XZLh`P2*6(NgS}qGcfGXVWpwsp#Rs}IuKbk*`2}&) zI^Vsk6S&Q4@oYS?dJ`NwMVBs6f57+RxdqVub#PvMu?$=^OJy5xEl0<5SLsSRy%%a0 zi}Y#1-F3m;Ieh#Y12UgW?-R)|eX>ZuF-2cc!1>~NS|XSF-6In>zBoZg+ml!6%fk7U zw0LHcz8VQk(jOJ+Yu)|^|15ufl$KQd_1eUZZzj`aC%umU6F1&D5XVWce_wAe(qCSZ zpX-QF4e{EmEVN9~6%bR5U*UT{eMHfcUo`jw*u?4r2s_$`}U{?NjvEm(u&<>B|%mq$Q3weshxk z76<``8vh{+nX`@9CB6IE&z)I%IFjR^LH{s1p|eppv=x za(g_jLU|xjWMAn-V7th$f({|LG8zzIE0g?cyW;%Dmtv%C+0@xVxPE^ zyZzi9P%JAD6ynwHptuzP`Kox7*9h7XSMonCalv;Md0i9Vb-c*!f0ubfk?&T&T}AHh z4m8Bz{JllKcdNg?D^%a5MFQ;#1z|*}H^qHLzW)L}wp?2tY7RejtSh8<;Zw)QGJYUm z|MbTxyj*McKlStlT9I5XlSWtQGN&-LTr2XyNU+`490rg?LYLMRnz-@oKqT1hpCGqP zyRXt4=_Woj$%n5ee<3zhLF>5>`?m9a#xQH+Jk_+|RM8Vi;2*XbK- zEL6sCpaGPzP>k8f4Kh|##_imt#zJMB;ir|JrMPGW`rityK1vHXMLy18%qmMQAm4WZ zP)i30KR&5vs15)C+8dM66&$k~i|ZT;KR&5vs15)C+8dJ(sAmGPijyIz6_bsqKLSFH zlOd=TljEpH0>h4zA*dCTK&emy#FCRCs1=i^sZ9bFmXjf<6_X39E(XY)00000#N437 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e5..9355b4155 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From dbcbbcb41a491dab7522d6754a1d0259f5fcb51c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 10:15:14 +0200 Subject: [PATCH 125/180] ci: projectVersion=4.11.0-SNAPSHOT [ci skip] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5bf62c858..e03ea1239 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.10.1-SNAPSHOT +projectVersion=4.11.0-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From 02a865853c26dd33cfcf13fb4699a1148e779dac Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 10:50:56 +0200 Subject: [PATCH 126/180] core 4.6.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1eb31cbb3..3598e7f04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.6.1" +micronaut = "4.6.2" micronaut-docs = "2.0.0" micronaut-test = "4.4.0" From 3a04b6fbb33987fd3475b09f260413da8995f8cc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 10:53:36 +0200 Subject: [PATCH 127/180] test 4.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3598e7f04..5823e005e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] 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" From 383db0009a4d8e159f1b5af6a992f299af06bed2 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 10:53:48 +0200 Subject: [PATCH 128/180] security 4.10.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5823e005e..3ce115d7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ 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-validation = "4.7.0" From cba365578e38c8686001a992d0b9da76b468dd76 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 10:54:00 +0200 Subject: [PATCH 129/180] logging 1.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ce115d7e..c406e29c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ micronaut-session = "4.3.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" From d28b36fad91d24751b0ea7e0378e6195cfec8498 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 10:54:11 +0200 Subject: [PATCH 130/180] session 4.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c406e29c9..0dc08fec5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ managed-jetty = '11.0.22' micronaut-reactor = "3.5.0" 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" From 7e328d4084b7a3cb4f794cab200373988507324c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 11:08:16 +0200 Subject: [PATCH 131/180] test: annotate test with @StepWise It fails otherwise when run in parallel --- .../test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy | 2 ++ 1 file changed, 2 insertions(+) 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 index b2da6492b..43d0175ca 100644 --- 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 @@ -6,7 +6,9 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.* import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Stepwise +@Stepwise @MicronautTest class SimpleServerSpec extends BaseServerlessApplicationSpec { From b4d688a679cbdd9b1b9cdb9c7f28d4f61b9b75fc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 22 Aug 2024 11:54:39 +0200 Subject: [PATCH 132/180] 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 From 3d96bbc4bc20f53c734d4f7ffa9049212fdb7221 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 22 Aug 2024 09:55:59 +0000 Subject: [PATCH 133/180] [skip ci] Release v4.10.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5bf62c858..dcc38ae23 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.10.1-SNAPSHOT +projectVersion=4.10.1 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 5fc5c33f11079924aae865a7769fc7477c6d3718 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Thu, 22 Aug 2024 10:00:29 +0000 Subject: [PATCH 134/180] chore: Bump version to 4.10.2-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index dcc38ae23..8904b38f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.10.1 +projectVersion=4.10.2-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From aae9642bdeda19e6ba467202d2871b0ea0ed02fb Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Aug 2024 18:03:31 +0200 Subject: [PATCH 135/180] remove unused import --- .../servlet/annotation/processor/ServletAnnotationVisitor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java index 6c004c7cc..2c1daaa35 100644 --- a/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java +++ b/servlet-processor/src/main/java/io/micronaut/servlet/annotation/processor/ServletAnnotationVisitor.java @@ -18,7 +18,6 @@ import static io.micronaut.core.util.ArrayUtils.concat; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.processing.ProcessingException; From fb02e95cc0a4ef5e199b950d15492e123fb40591 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 23 Aug 2024 18:05:35 +0200 Subject: [PATCH 136/180] core 4.6.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0dc08fec5..a60c29236 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.6.2" +micronaut = "4.6.3" micronaut-docs = "2.0.0" micronaut-test = "4.5.0" From dd177e56aa8546097fe3d9fad1400d3a81f20d86 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 20 Aug 2024 16:38:55 -0400 Subject: [PATCH 137/180] Improve the sample by using native-image and adding serialization case --- test-sample-poja/build.gradle | 16 +++++++++++++--- .../http/poja/sample/TestController.java | 8 ++++++++ .../micronaut/http/poja/sample/model/Cactus.java | 10 ++++++++++ .../http/poja/sample/SimpleServerSpec.groovy | 14 +++++++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index d1148b6b3..b638779e9 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -17,16 +17,16 @@ plugins { id("io.micronaut.build.internal.servlet.base") id("application") id("groovy") + id 'org.graalvm.buildtools.native' } dependencies { implementation(projects.micronautHttpPojaApache) implementation(mnLogging.slf4j.simple) - implementation(mn.micronaut.jackson.databind) + implementation(mnSerde.micronaut.serde.jackson) annotationProcessor(mn.micronaut.inject.java) testImplementation(projects.micronautHttpPojaTest) - testImplementation(mn.micronaut.jackson.databind) testImplementation(mnTest.micronaut.test.spock) testImplementation(mn.micronaut.inject.groovy.test) @@ -34,12 +34,22 @@ dependencies { testImplementation(mn.micronaut.inject.groovy) } -run { +application { mainClass.set("io.micronaut.http.poja.sample.Application") +} + +run { standardInput = System.in standardOutput = System.out } +graalvmNative { + binaries.all { + buildArgs.add("--gc=serial") + buildArgs.add("--install-exit-handlers") + } +} + test { useJUnitPlatform() } diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java index 96d511c73..1280a2f56 100644 --- a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/TestController.java @@ -23,8 +23,10 @@ import io.micronaut.http.annotation.Delete; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; import io.micronaut.http.annotation.Put; import io.micronaut.http.annotation.Status; +import io.micronaut.http.poja.sample.model.Cactus; /** * A controller for testing. @@ -56,5 +58,11 @@ public final String update(@NonNull String name) { return "Hello, " + name + "!\n"; } + @Get("/cactus") + @Produces(MediaType.APPLICATION_JSON) + public final Cactus getCactus() { + return new Cactus("green", 1); + } + } diff --git a/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java new file mode 100644 index 000000000..27fdd8772 --- /dev/null +++ b/test-sample-poja/src/main/java/io/micronaut/http/poja/sample/model/Cactus.java @@ -0,0 +1,10 @@ +package io.micronaut.http.poja.sample.model; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public record Cactus( + String color, + int spikeSize +) { +} diff --git a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy index 8fc6cbd2a..2b2c3bb04 100644 --- a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy +++ b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy @@ -6,7 +6,9 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client -import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.poja.sample.model.Cactus +import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.spock.annotation.MicronautTest; import jakarta.inject.Inject; import spock.lang.Specification; @@ -68,4 +70,14 @@ class SimpleServerSpec extends Specification { response.getBody(String.class).get() == "Hello, Andriy!\n" } + void "test GET method with serialization"() { + when: + HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/cactus").header("Host", "h")) + + then: + response.status == HttpStatus.OK + response.contentType.get() == MediaType.APPLICATION_JSON_TYPE + response.getBody(Cactus.class).get() == new Cactus("green", 1) + } + } From f4054553982c3d535e4e13ebad5768f1df40fc84 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 21 Aug 2024 11:48:02 -0400 Subject: [PATCH 138/180] Use Apache APIs for body validation --- .../llhttp/ApacheServerlessApplication.java | 6 +- .../poja/llhttp/ApacheServletHttpRequest.java | 59 +++++++++++-------- .../servlet/undertow/UndertowFactory.java | 2 +- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java index 4afaf6a8b..a1e1f2cc3 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java @@ -17,6 +17,7 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.buffer.ByteBufferFactory; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.codec.MediaTypeCodecRegistry; @@ -26,6 +27,7 @@ import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.servlet.http.ByteArrayBufferFactory; import io.micronaut.servlet.http.ServletHttpHandler; import jakarta.inject.Singleton; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -53,6 +55,7 @@ public class ApacheServerlessApplication private final ConversionService conversionService; private final MediaTypeCodecRegistry codecRegistry; private final ExecutorService ioExecutor; + private final ByteBufferFactory byteBufferFactory; private final ApacheServletConfiguration configuration; /** @@ -68,6 +71,7 @@ public ApacheServerlessApplication(ApplicationContext applicationContext, codecRegistry = applicationContext.getBean(MediaTypeCodecRegistry.class); ioExecutor = applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING)); configuration = applicationContext.getBean(ApacheServletConfiguration.class); + byteBufferFactory = ByteArrayBufferFactory.INSTANCE; } @Override @@ -79,7 +83,7 @@ protected void handleSingleRequest( ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); try { ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, response, configuration + in, conversionService, codecRegistry, ioExecutor, byteBufferFactory, response, configuration ); servletHttpHandler.service(exchange); } catch (ApacheServletBadRequestException e) { diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java index d4683758e..aadccf054 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java @@ -18,32 +18,34 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.buffer.ByteBufferFactory; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpMethod; import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpParameters; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.body.ByteBody; +import io.micronaut.http.body.stream.InputStreamByteBody; import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; import io.micronaut.http.poja.PojaHttpRequest; import io.micronaut.http.poja.llhttp.exception.ApacheServletBadRequestException; -import io.micronaut.http.poja.util.LimitingInputStream; import io.micronaut.http.poja.util.MultiValueHeaders; import io.micronaut.http.poja.util.MultiValuesQueryParameters; import io.micronaut.http.simple.cookies.SimpleCookies; -import io.micronaut.servlet.http.body.InputStreamByteBody; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.impl.io.ChunkedInputStream; +import org.apache.hc.core5.http.impl.io.ContentLengthInputStream; import org.apache.hc.core5.http.impl.io.DefaultHttpRequestParser; import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; +import org.apache.hc.core5.http.io.entity.EmptyInputStream; import org.apache.hc.core5.net.URIBuilder; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -67,6 +69,8 @@ @Internal public final class ApacheServletHttpRequest extends PojaHttpRequest { + private static final String TRANSFER_ENCODING_CHUNKED = "chunked"; + private final ClassicHttpRequest request; private final HttpMethod method; @@ -92,6 +96,7 @@ public ApacheServletHttpRequest( ConversionService conversionService, MediaTypeCodecRegistry codecRegistry, ExecutorService ioExecutor, + ByteBufferFactory byteBufferFactory, ApacheServletHttpResponse response, ApacheServletConfiguration configuration ) { @@ -119,29 +124,33 @@ public ApacheServletHttpRequest( long contentLength = getContentLength(); OptionalLong optionalContentLength = contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty(); - try { - InputStream bodyStream = inputStream; - if (sessionInputBuffer.length() > 0) { - byte[] data = new byte[sessionInputBuffer.length()]; - sessionInputBuffer.read(data, inputStream); - - bodyStream = new CombinedInputStream( - new ByteArrayInputStream(data), - inputStream - ); - } - if (contentLength > 0) { - bodyStream = new LimitingInputStream(bodyStream, contentLength); - } else { - // Empty - bodyStream = new ByteArrayInputStream(new byte[0]); - } - byteBody = InputStreamByteBody.create( - bodyStream, optionalContentLength, ioExecutor - ); - } catch (IOException e) { - throw new ApacheServletBadRequestException("Could not parse request body", e); + InputStream bodyStream = createBodyStream(inputStream, contentLength, sessionInputBuffer); + byteBody = InputStreamByteBody.create( + bodyStream, optionalContentLength, ioExecutor, byteBufferFactory + ); + } + + /** + * Create body stream. + * Based on org.apache.hc.core5.http.impl.io.BHttpConnectionBase#createContentOutputStream. + * + * @param inputStream The input stream + * @param contentLength The content length + * @param sessionInputBuffer The input buffer + * @return The body stream + */ + private InputStream createBodyStream(InputStream inputStream, long contentLength, SessionInputBufferImpl sessionInputBuffer) { + InputStream bodyStream; + if (contentLength > 0) { + bodyStream = new ContentLengthInputStream(sessionInputBuffer, inputStream, contentLength); + } else if (contentLength == 0) { + bodyStream = EmptyInputStream.INSTANCE; + } else if (TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(headers.get(HttpHeaders.TRANSFER_ENCODING))) { + bodyStream = new ChunkedInputStream(sessionInputBuffer, inputStream); + } else { + bodyStream = EmptyInputStream.INSTANCE; } + return bodyStream; } @Override diff --git a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java index 3576ef784..8ddbf930f 100644 --- a/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java +++ b/http-server-undertow/src/main/java/io/micronaut/servlet/undertow/UndertowFactory.java @@ -263,7 +263,7 @@ protected Undertow undertowServer(Undertow.Builder builder) { * * @param servletConfiguration The servlet configuration. * @return The deployment info - * @deprecated Use {@link ##deploymentInfo(MicronautServletConfiguration, Collection)} + * @deprecated Use {@link #deploymentInfo(MicronautServletConfiguration, Collection)} */ @Deprecated(forRemoval = true, since = "4.8.0") protected DeploymentInfo deploymentInfo(MicronautServletConfiguration servletConfiguration) { From 9e9a23c96573b58aa32ec83667ed9acf0e10397c Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 23 Aug 2024 12:59:16 -0400 Subject: [PATCH 139/180] Rewrite test to Java --- test-sample-poja/build.gradle | 6 +- .../http/poja/sample/SimpleServerSpec.groovy | 83 ------------------ .../http/poja/sample/SimpleServerTest.java | 87 +++++++++++++++++++ 3 files changed, 89 insertions(+), 87 deletions(-) delete mode 100644 test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy create mode 100644 test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index b638779e9..1c3390f9a 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -16,7 +16,6 @@ plugins { id("io.micronaut.build.internal.servlet.base") id("application") - id("groovy") id 'org.graalvm.buildtools.native' } @@ -27,11 +26,10 @@ dependencies { annotationProcessor(mn.micronaut.inject.java) testImplementation(projects.micronautHttpPojaTest) - testImplementation(mnTest.micronaut.test.spock) + testImplementation(mnTest.micronaut.test.junit5) - testImplementation(mn.micronaut.inject.groovy.test) + testAnnotationProcessor(mn.micronaut.inject.java.test) testImplementation(mn.micronaut.inject.java) - testImplementation(mn.micronaut.inject.groovy) } application { diff --git a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy b/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy deleted file mode 100644 index 2b2c3bb04..000000000 --- a/test-sample-poja/src/test/groovy/io/micronaut/http/poja/sample/SimpleServerSpec.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package io.micronaut.http.poja.sample - -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus -import io.micronaut.http.MediaType; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.http.poja.sample.model.Cactus -import io.micronaut.serde.annotation.Serdeable; -import io.micronaut.test.extensions.spock.annotation.MicronautTest; -import jakarta.inject.Inject; -import spock.lang.Specification; - -@MicronautTest -class SimpleServerSpec extends Specification { - - @Inject - @Client("/") - HttpClient client - - void "test GET method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/").header("Host", "h")) - - then: - response.status == HttpStatus.OK - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == 'Hello, Micronaut Without Netty!\n' - } - - void "test invalid GET method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/test/invalid").header("Host", "h")) - - then: - var e = thrown(HttpClientResponseException) - e.status == HttpStatus.NOT_FOUND - e.response.contentType.get() == MediaType.APPLICATION_JSON_TYPE - e.response.getBody(String.class).get().length() > 0 - } - - void "test DELETE method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.DELETE("/").header("Host", "h")) - - then: - response.status() == HttpStatus.OK - response.getBody(String.class).isEmpty() - } - - void "test POST method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.POST("/Andriy", null).header("Host", "h")) - - then: - response.status() == HttpStatus.CREATED - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == "Hello, Andriy\n" - } - - void "test PUT method"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.PUT("/Andriy", null).header("Host", "h")) - - then: - response.status() == HttpStatus.OK - response.contentType.get() == MediaType.TEXT_PLAIN_TYPE - response.getBody(String.class).get() == "Hello, Andriy!\n" - } - - void "test GET method with serialization"() { - when: - HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/cactus").header("Host", "h")) - - then: - response.status == HttpStatus.OK - response.contentType.get() == MediaType.APPLICATION_JSON_TYPE - response.getBody(Cactus.class).get() == new Cactus("green", 1) - } - -} diff --git a/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java new file mode 100644 index 000000000..f8b073693 --- /dev/null +++ b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java @@ -0,0 +1,87 @@ +package io.micronaut.http.poja.sample; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.poja.sample.model.Cactus; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest +public class SimpleServerTest { + + @Inject + @Client("/") + HttpClient client; + + @Test + void testGetMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.GET("/").header("Host", "h")); + + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getContentType().get()); + assertEquals("Hello, Micronaut Without Netty!\n", response.getBody(String.class).get()); + } + + @Test + void testInvalidGetMethod() { + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.GET("/test/invalid").header("Host", "h")); + }); + + assertEquals(HttpStatus.NOT_FOUND, e.getStatus()); + assertEquals(MediaType.APPLICATION_JSON_TYPE, e.getResponse().getContentType().get()); + assertTrue(e.getResponse().getBody(String.class).get().length() > 0); + } + + @Test + void testDeleteMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.DELETE("/").header("Host", "h")); + + assertEquals(HttpStatus.OK, response.status()); + response.getBody(String.class).isEmpty(); + } + + @Test + void testPostMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.POST("/Andriy", null).header("Host", "h")); + + assertEquals(HttpStatus.CREATED, response.status()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getContentType().get()); + assertEquals("Hello, Andriy\n", response.getBody(String.class).get()); + } + + @Test + void testPutMethod() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.PUT("/Andriy", null).header("Host", "h")); + + assertEquals(HttpStatus.OK, response.status()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getContentType().get()); + assertEquals("Hello, Andriy!\n", response.getBody(String.class).get()); + } + + @Test + void testGetMethodWithSerialization() { + HttpResponse response = client.toBlocking() + .exchange(HttpRequest.GET("/cactus").header("Host", "h")); + + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals(MediaType.APPLICATION_JSON_TYPE, response.getContentType().get()); + assertEquals(new Cactus("green", 1), response.getBody(Cactus.class).get()); ; + } + +} From ec94f82fdaaa10d2b18136254f26c783af9fe2cc Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 23 Aug 2024 18:39:20 +0000 Subject: [PATCH 140/180] [skip ci] Release v4.11.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e03ea1239..5af4bcb7d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.0-SNAPSHOT +projectVersion=4.11.0 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 18bc56f3418aa7ca38eb1bb5b2b8f3997c45a014 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 23 Aug 2024 18:43:32 +0000 Subject: [PATCH 141/180] chore: Bump version to 4.11.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5af4bcb7d..457bcd0aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.0 +projectVersion=4.11.1-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From ee10a1424594074c1d2c1e167c7a75758149df70 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Fri, 23 Aug 2024 16:39:16 -0400 Subject: [PATCH 142/180] Add documentation for HTTP POJA module --- .../ApacheServerlessApplication.java | 4 ++-- .../ApacheServletConfiguration.java | 2 +- .../ApacheServletHttpRequest.java | 4 ++-- .../ApacheServletHttpResponse.java | 2 +- .../exception/ApacheServletBadRequestException.java | 2 +- .../http/poja/BaseServerlessApplicationSpec.groovy | 2 +- src/main/docs/guide/httpPoja.adoc | 13 +++++++++++++ src/main/docs/guide/toc.yml | 1 + 8 files changed, 22 insertions(+), 8 deletions(-) rename http-poja-apache/src/main/java/io/micronaut/http/poja/{llhttp => apache}/ApacheServerlessApplication.java (97%) rename http-poja-apache/src/main/java/io/micronaut/http/poja/{llhttp => apache}/ApacheServletConfiguration.java (97%) rename http-poja-apache/src/main/java/io/micronaut/http/poja/{llhttp => apache}/ApacheServletHttpRequest.java (99%) rename http-poja-apache/src/main/java/io/micronaut/http/poja/{llhttp => apache}/ApacheServletHttpResponse.java (99%) rename http-poja-apache/src/main/java/io/micronaut/http/poja/{llhttp => apache}/exception/ApacheServletBadRequestException.java (96%) create mode 100644 src/main/docs/guide/httpPoja.adoc 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/apache/ApacheServerlessApplication.java similarity index 97% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java index a1e1f2cc3..83f87a0bd 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.llhttp; +package io.micronaut.http.poja.apache; import io.micronaut.context.ApplicationContext; import io.micronaut.core.convert.ConversionService; @@ -22,7 +22,7 @@ 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.poja.apache.exception.ApacheServletBadRequestException; import io.micronaut.http.server.exceptions.HttpServerException; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.runtime.ApplicationConfiguration; 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/apache/ApacheServletConfiguration.java similarity index 97% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletConfiguration.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java index 7740bde64..39ef109d6 100644 --- 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/apache/ApacheServletConfiguration.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.llhttp; +package io.micronaut.http.poja.apache; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.bind.annotation.Bindable; 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/apache/ApacheServletHttpRequest.java similarity index 99% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java index aadccf054..acebc2ef3 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.llhttp; +package io.micronaut.http.poja.apache; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -30,7 +30,7 @@ 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.apache.exception.ApacheServletBadRequestException; import io.micronaut.http.poja.util.MultiValueHeaders; import io.micronaut.http.poja.util.MultiValuesQueryParameters; import io.micronaut.http.simple.cookies.SimpleCookies; 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/apache/ApacheServletHttpResponse.java similarity index 99% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/ApacheServletHttpResponse.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java index 3f837cc9b..c84335001 100644 --- 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/apache/ApacheServletHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.llhttp; +package io.micronaut.http.poja.apache; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; 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/apache/exception/ApacheServletBadRequestException.java similarity index 96% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/llhttp/exception/ApacheServletBadRequestException.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java index 3efb664eb..0589f7796 100644 --- 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/apache/exception/ApacheServletBadRequestException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.http.poja.llhttp.exception; +package io.micronaut.http.poja.apache.exception; import io.micronaut.core.annotation.Internal; import io.micronaut.http.server.exceptions.HttpServerException; 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 index 1a3964558..ec81e1c79 100644 --- 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 @@ -2,7 +2,7 @@ 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.http.poja.apache.ApacheServerlessApplication import io.micronaut.runtime.ApplicationConfiguration import jakarta.inject.Inject import jakarta.inject.Singleton diff --git a/src/main/docs/guide/httpPoja.adoc b/src/main/docs/guide/httpPoja.adoc new file mode 100644 index 000000000..719c08120 --- /dev/null +++ b/src/main/docs/guide/httpPoja.adoc @@ -0,0 +1,13 @@ +HTTP POJA allows creating Micronaut applications that consume respond to HTTP requests with streams. By default, the application will read requests from standard input stream and write responses to standard output. The requests can only be answered in a serial manner. + +This feature allows creating simple applications that launch and respond on demand with minimal overhead. The module is suitable for usage with `systemd` on Linux or `launchd` on MacOS. + +To use the HTTP POJA feature add the following dependencies: + +dependency:io.micronaut.servlet:micronaut-http-poja-apache[] + +dependency:io.micronaut.servlet:micronaut-http-poja-test[scope="test"] + +To customize the HTTP POJA you can use the following configuration properties: + +include::{includedir}configurationProperties/io.micronaut.http.poja.apache.ApacheServletConfiguration.adoc[] diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 4c8558475..264b0d50b 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -10,6 +10,7 @@ warDeployment: jetty: Jetty Server tomcat: Tomcat Server undertow: Undertow Server +httpPoja: HTTP POJA Application knownIssues: Known Issues faq: FAQ breaks: Breaking Changes From 07cb14c0ee48f0e47d80f19bd948d0c1c9f559a4 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 26 Aug 2024 12:31:41 -0400 Subject: [PATCH 143/180] Fix the flaky HTTP POJA test --- http-poja-apache/build.gradle | 2 +- .../poja/BaseServerlessApplicationSpec.groovy | 104 ----------- .../http/poja/SimpleServerSpec.groovy | 173 +++++++++++------- 3 files changed, 108 insertions(+), 171 deletions(-) delete mode 100644 http-poja-apache/src/test/groovy/io/micronaut/http/poja/BaseServerlessApplicationSpec.groovy diff --git a/http-poja-apache/build.gradle b/http-poja-apache/build.gradle index 4ee8b902f..c5d310861 100644 --- a/http-poja-apache/build.gradle +++ b/http-poja-apache/build.gradle @@ -25,7 +25,7 @@ dependencies { compileOnly(mn.reactor) compileOnly(mn.micronaut.json.core) - + testImplementation(projects.micronautHttpPojaTest) testImplementation(mnSerde.micronaut.serde.jackson) } 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 ec81e1c79..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.apache.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 index 43d0175ca..fcff1a699 100644 --- 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 @@ -5,115 +5,122 @@ import io.micronaut.core.annotation.NonNull import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.* +import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.inject.Singleton +import spock.lang.Specification import spock.lang.Stepwise @Stepwise @MicronautTest -class SimpleServerSpec extends BaseServerlessApplicationSpec { +class SimpleServerSpec extends Specification { + + @Inject + StringTestingClient client void "test GET method"() { when: - app.write("""\ - GET /test HTTP/1.1 - Host: h - - """.stripIndent()) + var response = client.exchange(unindent(""" + GET /test HTTP/1.1\r + Host: h\r + \r + """)) then: - app.read() == """\ - HTTP/1.1 200 Ok - Content-Length: 32 - Content-Type: text/plain - + response == unindent(""" + HTTP/1.1 200 Ok\r + Content-Length: 32\r + Content-Type: text/plain\r + \r Hello, Micronaut Without Netty! - """.stripIndent() + """) } void "test invalid GET method"() { when: - app.write("""\ - GET /invalid-test HTTP/1.1 - Host: h - - """.stripIndent()) + var response = client.exchange(unindent(""" + GET /invalid-test HTTP/1.1\r + Host: h\r + \r + """)) 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() + response == unindent("""\ + HTTP/1.1 404 Not Found\r + Content-Length: 140\r + Content-Type: application/json\r + \r + {"_links":{"self":[{"href":"/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""") } void "test non-parseable GET method"() { when: - app.write("""\ - GET /test HTTP/1.1error - Host: h - - """.stripIndent()) + var response = client.exchange(unindent(""" + GET /test HTTP/1.1error\r + Host: h\r + \r + """)) then: - app.read() == """\ - HTTP/1.1 400 Bad Request - Content-Length: 32 - Content-Type: text/plain - - HTTP request could not be parsed""".stripIndent() + response == unindent(""" + HTTP/1.1 400 Bad Request\r + Content-Length: 32\r + Content-Type: text/plain\r + \r + HTTP request could not be parsed""") } void "test DELETE method"() { when: - app.write("""\ - DELETE /test HTTP/1.1 - Host: h - - """.stripIndent()) + var response = client.exchange(unindent(""" + DELETE /test HTTP/1.1\r + Host: h\r + \r + """)) then: - app.read() == """\ - HTTP/1.1 200 Ok - Content-Length: 0 - - """.stripIndent() + response == unindent(""" + HTTP/1.1 200 Ok\r + Content-Length: 0\r + \r + """) } void "test POST method"() { when: - app.write("""\ - POST /test/Dream HTTP/1.1 - Host: h - - """.stripIndent()) + var response = client.exchange(unindent(""" + POST /test/Dream HTTP/1.1\r + Host: h\r + \r + """)) then: - app.read() == """\ - HTTP/1.1 201 Created - Content-Length: 13 - Content-Type: text/plain - + response == unindent(""" + HTTP/1.1 201 Created\r + Content-Length: 13\r + Content-Type: text/plain\r + \r Hello, Dream - """.stripIndent() + """) } void "test PUT method"() { when: - app.write("""\ - PUT /test/Dream1 HTTP/1.1 - Host: h - - """.stripIndent()) + var response = client.exchange(unindent(""" + PUT /test/Dream1 HTTP/1.1\r + Host: h\r + \r + """)) then: - app.read() == """\ - HTTP/1.1 200 Ok - Content-Length: 15 - Content-Type: text/plain - + response == unindent(""" + HTTP/1.1 200 Ok\r + Content-Length: 15\r + Content-Type: text/plain\r + \r Hello, Dream1! - """.stripIndent() + """) } /** @@ -146,4 +153,38 @@ class SimpleServerSpec extends BaseServerlessApplicationSpec { } + private String unindent(String value, int indentSpaces = 8) { + while (value.charAt(0) == '\n' as char) { + value = value.substring(1) + } + var lines = value.split("\n") + .collect({ it.startsWith(" ".repeat(indentSpaces)) ? it.substring(indentSpaces) : it }) + .join("\n") + } + + @Singleton + static class StringTestingClient { + + private EmbeddedServer server + + StringTestingClient(EmbeddedServer server) { + this.server = server + } + + String exchange(String request) { + try (Socket socket = new Socket(server.host, server.port)) { + OutputStream output = socket.getOutputStream() + output.write(request.getBytes()) + + InputStream input = socket.getInputStream() + return new String(input.readAllBytes()) + .replaceAll("Date:[^\r]+\r\n", "") + } catch (IOException ex) { + System.out.println("Could not exchange request with server: " + ex.getMessage()); + ex.printStackTrace(); + } + } + + } + } From 540da799add5c41ced67f1b9a953aa8c2eaf19fe Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 26 Aug 2024 12:37:05 -0400 Subject: [PATCH 144/180] Rename http-poja-apache to simply http-poja --- http-poja-test/build.gradle | 4 ++-- {http-poja-apache => http-poja}/build.gradle | 0 .../http/poja/apache/ApacheServerlessApplication.java | 0 .../http/poja/apache/ApacheServletConfiguration.java | 0 .../micronaut/http/poja/apache/ApacheServletHttpRequest.java | 0 .../micronaut/http/poja/apache/ApacheServletHttpResponse.java | 0 .../apache/exception/ApacheServletBadRequestException.java | 0 .../META-INF/services/io.micronaut.http.HttpResponseFactory | 0 .../groovy/io/micronaut/http/poja/SimpleServerSpec.groovy | 0 .../src/test/resources/logback.xml | 0 settings.gradle | 4 ++-- src/main/docs/guide/httpPoja.adoc | 3 ++- test-sample-poja/build.gradle | 3 ++- .../build.gradle | 2 +- .../http/server/tck/poja/PojaApacheServerTestSuite.java | 0 .../http/server/tck/poja/PojaApacheServerUnderTest.java | 0 .../server/tck/poja/PojaApacheServerUnderTestProvider.java | 0 .../reflect-config.json | 0 .../resource-config.json | 0 .../services/io.micronaut.http.tck.ServerUnderTestProvider | 0 20 files changed, 9 insertions(+), 7 deletions(-) rename {http-poja-apache => http-poja}/build.gradle (100%) rename {http-poja-apache => http-poja}/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java (100%) rename {http-poja-apache => http-poja}/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java (100%) rename {http-poja-apache => http-poja}/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java (100%) rename {http-poja-apache => http-poja}/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java (100%) rename {http-poja-apache => http-poja}/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java (100%) rename {http-poja-apache => http-poja}/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory (100%) rename {http-poja-apache => http-poja}/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy (100%) rename {http-poja-apache => http-poja}/src/test/resources/logback.xml (100%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/build.gradle (78%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java (100%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java (100%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java (100%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json (100%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json (100%) rename {test-suite-http-server-tck-poja-apache => test-suite-http-server-tck-poja}/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider (100%) diff --git a/http-poja-test/build.gradle b/http-poja-test/build.gradle index 93906ee35..d84139f46 100644 --- a/http-poja-test/build.gradle +++ b/http-poja-test/build.gradle @@ -20,10 +20,10 @@ plugins { dependencies { implementation(projects.micronautHttpPojaCommon) api(mn.micronaut.inject.java) - api(mn.micronaut.http.client) + testImplementation(mn.micronaut.http.client) testImplementation(mn.micronaut.jackson.databind) - testImplementation(projects.micronautHttpPojaApache) + testImplementation(projects.micronautHttpPoja) } micronautBuild { diff --git a/http-poja-apache/build.gradle b/http-poja/build.gradle similarity index 100% rename from http-poja-apache/build.gradle rename to http-poja/build.gradle diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java b/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java similarity index 100% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java rename to http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java b/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java similarity index 100% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java rename to http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java b/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java similarity index 100% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java rename to http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java b/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java similarity index 100% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java rename to http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java b/http-poja/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java similarity index 100% rename from http-poja-apache/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java rename to http-poja/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java diff --git a/http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory b/http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory similarity index 100% rename from http-poja-apache/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory rename to http-poja/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory diff --git a/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy b/http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy similarity index 100% rename from http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy rename to http-poja/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy diff --git a/http-poja-apache/src/test/resources/logback.xml b/http-poja/src/test/resources/logback.xml similarity index 100% rename from http-poja-apache/src/test/resources/logback.xml rename to http-poja/src/test/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index 0fe362eff..ef236bc5e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,11 +32,11 @@ include 'http-server-jetty' include 'http-server-undertow' include 'http-server-tomcat' include 'http-poja-common' -include 'http-poja-apache' +include 'http-poja' 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-http-server-tck-poja' include 'test-suite-kotlin-jetty' include 'test-sample-poja' diff --git a/src/main/docs/guide/httpPoja.adoc b/src/main/docs/guide/httpPoja.adoc index 719c08120..90aaed340 100644 --- a/src/main/docs/guide/httpPoja.adoc +++ b/src/main/docs/guide/httpPoja.adoc @@ -1,10 +1,11 @@ HTTP POJA allows creating Micronaut applications that consume respond to HTTP requests with streams. By default, the application will read requests from standard input stream and write responses to standard output. The requests can only be answered in a serial manner. +Currently HTTP POJA is based on https://hc.apache.org/httpcomponents-core-5.2.x/[Apache HTTP Core library]. This feature allows creating simple applications that launch and respond on demand with minimal overhead. The module is suitable for usage with `systemd` on Linux or `launchd` on MacOS. To use the HTTP POJA feature add the following dependencies: -dependency:io.micronaut.servlet:micronaut-http-poja-apache[] +dependency:io.micronaut.servlet:micronaut-http-poja[] dependency:io.micronaut.servlet:micronaut-http-poja-test[scope="test"] diff --git a/test-sample-poja/build.gradle b/test-sample-poja/build.gradle index 1c3390f9a..d903be58a 100644 --- a/test-sample-poja/build.gradle +++ b/test-sample-poja/build.gradle @@ -20,13 +20,14 @@ plugins { } dependencies { - implementation(projects.micronautHttpPojaApache) + implementation(projects.micronautHttpPoja) implementation(mnLogging.slf4j.simple) implementation(mnSerde.micronaut.serde.jackson) annotationProcessor(mn.micronaut.inject.java) testImplementation(projects.micronautHttpPojaTest) testImplementation(mnTest.micronaut.test.junit5) + testImplementation(mn.micronaut.http.client) testAnnotationProcessor(mn.micronaut.inject.java.test) testImplementation(mn.micronaut.inject.java) diff --git a/test-suite-http-server-tck-poja-apache/build.gradle b/test-suite-http-server-tck-poja/build.gradle similarity index 78% rename from test-suite-http-server-tck-poja-apache/build.gradle rename to test-suite-http-server-tck-poja/build.gradle index d8050432a..978d581b3 100644 --- a/test-suite-http-server-tck-poja-apache/build.gradle +++ b/test-suite-http-server-tck-poja/build.gradle @@ -4,6 +4,6 @@ plugins { dependencies { testRuntimeOnly(mnValidation.micronaut.validation) - testImplementation(projects.micronautHttpPojaApache) + testImplementation(projects.micronautHttpPoja) 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/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java similarity index 100% rename from test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java rename to test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java 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/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java similarity index 100% rename from test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java rename to test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java 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/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java similarity index 100% rename from test-suite-http-server-tck-poja-apache/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java rename to test-suite-http-server-tck-poja/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java 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/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json similarity index 100% rename from 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 rename to test-suite-http-server-tck-poja/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/reflect-config.json 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/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json similarity index 100% rename from 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 rename to test-suite-http-server-tck-poja/src/test/resources/META-INF/native-image/io/micronaut/servlet/test-suite-http-server-tck-poja-apache/resource-config.json 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/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider similarity index 100% rename from test-suite-http-server-tck-poja-apache/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider rename to test-suite-http-server-tck-poja/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider From 29cea09c4046b0d653f713ef08e1100ebb6ff097 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 26 Aug 2024 13:07:23 -0400 Subject: [PATCH 145/180] Revert renaming module because of broken binary compatability --- {http-poja => http-poja-apache}/build.gradle | 0 .../http/poja/apache/ApacheServerlessApplication.java | 0 .../http/poja/apache/ApacheServletConfiguration.java | 0 .../micronaut/http/poja/apache/ApacheServletHttpRequest.java | 1 + .../micronaut/http/poja/apache/ApacheServletHttpResponse.java | 0 .../apache/exception/ApacheServletBadRequestException.java | 0 .../META-INF/services/io.micronaut.http.HttpResponseFactory | 0 .../groovy/io/micronaut/http/poja/SimpleServerSpec.groovy | 0 .../src/test/resources/logback.xml | 0 http-poja-test/build.gradle | 2 +- settings.gradle | 4 ++-- src/main/docs/guide/httpPoja.adoc | 2 +- test-sample-poja/build.gradle | 2 +- .../build.gradle | 2 +- .../http/server/tck/poja/PojaApacheServerTestSuite.java | 0 .../http/server/tck/poja/PojaApacheServerUnderTest.java | 0 .../server/tck/poja/PojaApacheServerUnderTestProvider.java | 0 .../reflect-config.json | 0 .../resource-config.json | 0 .../services/io.micronaut.http.tck.ServerUnderTestProvider | 0 20 files changed, 7 insertions(+), 6 deletions(-) rename {http-poja => http-poja-apache}/build.gradle (100%) rename {http-poja => http-poja-apache}/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java (100%) rename {http-poja => http-poja-apache}/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java (100%) rename {http-poja => http-poja-apache}/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java (99%) rename {http-poja => http-poja-apache}/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpResponse.java (100%) rename {http-poja => http-poja-apache}/src/main/java/io/micronaut/http/poja/apache/exception/ApacheServletBadRequestException.java (100%) rename {http-poja => http-poja-apache}/src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory (100%) rename {http-poja => http-poja-apache}/src/test/groovy/io/micronaut/http/poja/SimpleServerSpec.groovy (100%) rename {http-poja => http-poja-apache}/src/test/resources/logback.xml (100%) rename {test-suite-http-server-tck-poja => test-suite-http-server-tck-poja-apache}/build.gradle (78%) rename {test-suite-http-server-tck-poja => test-suite-http-server-tck-poja-apache}/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerTestSuite.java (100%) rename {test-suite-http-server-tck-poja => test-suite-http-server-tck-poja-apache}/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTest.java (100%) rename {test-suite-http-server-tck-poja => test-suite-http-server-tck-poja-apache}/src/test/java/io/micronaut/http/server/tck/poja/PojaApacheServerUnderTestProvider.java (100%) rename {test-suite-http-server-tck-poja => 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 (100%) rename {test-suite-http-server-tck-poja => 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 (100%) rename {test-suite-http-server-tck-poja => test-suite-http-server-tck-poja-apache}/src/test/resources/META-INF/services/io.micronaut.http.tck.ServerUnderTestProvider (100%) diff --git a/http-poja/build.gradle b/http-poja-apache/build.gradle similarity index 100% rename from http-poja/build.gradle rename to http-poja-apache/build.gradle diff --git a/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java similarity index 100% rename from http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java diff --git a/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java similarity index 99% rename from http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java rename to http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java index acebc2ef3..7cec58f3a 100644 --- a/http-poja/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java @@ -88,6 +88,7 @@ public final class ApacheServletHttpRequest extends PojaHttpRequest Date: Mon, 26 Aug 2024 15:15:12 -0400 Subject: [PATCH 146/180] Modify the sample to run tests in order --- .../groovy/io/micronaut/http/poja/SimpleServerSpec.groovy | 5 ++--- .../java/io/micronaut/http/poja/sample/SimpleServerTest.java | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) 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 index fcff1a699..0eea38030 100644 --- 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 @@ -179,9 +179,8 @@ class SimpleServerSpec extends Specification { InputStream input = socket.getInputStream() return new String(input.readAllBytes()) .replaceAll("Date:[^\r]+\r\n", "") - } catch (IOException ex) { - System.out.println("Could not exchange request with server: " + ex.getMessage()); - ex.printStackTrace(); + } catch (IOException e) { + throw new RuntimeException("Could not exchange request with server", e) } } diff --git a/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java index f8b073693..df6038f23 100644 --- a/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java +++ b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java @@ -10,13 +10,16 @@ import io.micronaut.http.poja.sample.model.Cactus; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @MicronautTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class SimpleServerTest { @Inject From 688b387ec1c360db50bf899b1aa97ec688757d70 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 26 Aug 2024 18:19:32 -0400 Subject: [PATCH 147/180] Add a configuration option whether to use inherited channel --- .../poja/apache/ApacheServerlessApplication.java | 4 ++++ .../poja/apache/ApacheServletConfiguration.java | 6 +++++- .../http/poja/PojaHttpServerlessApplication.java | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java index 83f87a0bd..f4c291609 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java @@ -111,4 +111,8 @@ private void writeResponse(ClassicHttpResponse response, OutputStream out) throw out.flush(); } + @Override + protected boolean useInheritedChannel() { + return configuration.useInheritedChannel(); + } } diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java index 39ef109d6..aa5fb62d9 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletConfiguration.java @@ -25,6 +25,8 @@ * (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). + * @param useInheritedChannel When true, the inherited channel will be used by if present. + * Otherwise, STDIN and STDOUT will be used. * @author Andriy Dmytruk * @since 4.10.0 */ @@ -33,7 +35,9 @@ public record ApacheServletConfiguration( @Bindable(defaultValue = "8192") int inputBufferSize, @Bindable(defaultValue = "8192") - int outputBufferSize + int outputBufferSize, + @Bindable(defaultValue = "true") + boolean useInheritedChannel ) { } 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 index cb14fdc02..603023914 100644 --- 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 @@ -99,7 +99,10 @@ protected ServletExchange createExchange(Object request, Object respon try { // Default streams to streams based on System.inheritedChannel. // If not possible, use System.in/out. - Channel channel = System.inheritedChannel(); + Channel channel = null; + if (useInheritedChannel()) { + channel = System.inheritedChannel(); + } if (channel != null) { try (InputStream in = Channels.newInputStream((ReadableByteChannel) channel); OutputStream out = Channels.newOutputStream((WritableByteChannel) channel)) { @@ -146,6 +149,16 @@ protected abstract void handleSingleRequest( OutputStream out ) throws IOException; + /** + * Whether to use the inherited channel by default. + * If false, STDIN and STDOUT will be used directly instead. + * + * @return Whether to use the inherited channel + */ + protected boolean useInheritedChannel() { + return true; + } + @Override public @NonNull PojaHttpServerlessApplication stop() { return EmbeddedApplication.super.stop(); From 11f2427e9a4f603bac099d8ed7928a9c45276740 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 28 Aug 2024 17:16:51 -0400 Subject: [PATCH 148/180] Add systemd and launchd configuration information to the documentation --- src/main/docs/guide/httpPoja.adoc | 150 +++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/src/main/docs/guide/httpPoja.adoc b/src/main/docs/guide/httpPoja.adoc index c61586db9..366d24c0b 100644 --- a/src/main/docs/guide/httpPoja.adoc +++ b/src/main/docs/guide/httpPoja.adoc @@ -1,7 +1,7 @@ HTTP POJA allows creating Micronaut applications that consume respond to HTTP requests with streams. By default, the application will read requests from standard input stream and write responses to standard output. The requests can only be answered in a serial manner. Currently HTTP POJA is based on https://hc.apache.org/httpcomponents-core-5.2.x/[Apache HTTP Core library]. -This feature allows creating simple applications that launch and respond on demand with minimal overhead. The module is suitable for usage with `systemd` on Linux or `launchd` on MacOS. +This feature allows creating simple applications that launch and respond on demand with minimal overhead. The module is suitable for usage with `systemd` on Linux or `launchd` on MacOS. Examples are given below. To use the HTTP POJA feature add the following dependencies: @@ -12,3 +12,151 @@ dependency:io.micronaut.servlet:micronaut-http-poja-test[scope="test"] To customize the HTTP POJA you can use the following configuration properties: include::{includedir}configurationProperties/io.micronaut.http.poja.apache.ApacheServletConfiguration.adoc[] + +=== Use HTTP POJA with launchd on MacOS + +If you have built a HTTP POJA application as a native image executable, create the following `plist` file and +replace `[executable]` with your executable path. + +NOTE: If you are unfamiliar with building native image executables refer to https://guides.micronaut.io/latest/micronaut-creating-first-graal-app[Micronaut Creating First Graal App] guide. + +NOTE: If you do not wish to use native image prepend `java` and `-jar` program arguments and use the jar instead. + +.~/Library/LaunchAgents/com.example.poja.plist +[source,xml] +---- + + + + + Label + com.example.poja + + Enabled + + + ProgramArguments + + [executable] + -Dpoja.apache.useInheritedChannel=false + + + Sockets + + Listeners + + SockServiceName + 8080 + SockType + stream + SockProtocol + TCP + + + + StandardErrorPath + /tmp/com.example.poja.log + + inetdCompatibility + + Wait + + + + KeepAlive + + + +---- + +Load the `plist` file with launchd: +[source, bash] +---- +launchctl load ~/Library/LaunchAgents/com.example.poja.plist +---- + +Then the configured application will respond on port `8080`: +[source, bash] +---- +curl localhost:8080 +---- + +=== Use HTTP POJA with systemd on Linux + +If you have built a HTTP POJA application as a native image executable, create the following files and +replace `[executable]` with your executable path. + +NOTE: If you are unfamiliar with building native image executables refer to https://guides.micronaut.io/latest/micronaut-creating-first-graal-app[Micronaut Creating First Graal App] guide. + +NOTE: If you do not wish to use native image prepend `java` and `-jar` program arguments and use the jar instead. + +./etc/systemd/system/examplepoja.socket +[source, toml] +---- +[Unit] +Description=Socket to launch poja example on incoming connection + +[Socket] +ListenStream=127.0.0.1:8080 +Accept=yes + +[Install] +WantedBy=sockets.target +---- + + +./etc/systemd/system/examplepoja@.service +[source, toml] +---- +[Unit] +Description=Example Poja Service +Requires=examplepoja.socket + +[Service] +Type=simple +ExecStart=[executable] -Dpoja.apache.useInheritedChannel=false +ExecStop=/bin/kill $MAINPID +KillMode=process +StandardInput=socket +StandardOutput=socket +StandardError=journal + +[Install] +WantedBy=multi-user.target +---- + +Change selinux policy to allow systemd to use executable in the desired location with: +[source, bash] +---- +chcon -R -t bin_t [executable parent directory] +---- + +Enable and start listening on the socket with systemctl: +[source, bash] +---- +sudo systemctl enable examplepoja.socket +sudo systemctl start examplepoja.socket +---- + +Then the configured application will respond on port `8080`: +[source, bash] +---- +curl localhost:8080 +---- + +==== Use HTTP POJA with `systemd-socket-activate` on Linux + +To test your application with `systemd-socket-activate` run: + +[source, bash] +---- +systemd-socket-activate --inetd -a -l /tmp/http-poja.sock [executable] +---- + +In a separate terminal send a request to the socket: + +[source, bash] +---- +curl --unix-socket /tmp/http-poja.sock http://localhost/ +---- + From fb963f28db0be6053d556ab148d8a9079c994baa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:27:08 -0400 Subject: [PATCH 149/180] Update managed.jetty to v11.0.23 (#789) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a60c29236..c3bfe91cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ tomcat = '10.1.28' bcpkix = "1.70" managed-apache-http-core5 = "5.2.5" -managed-jetty = '11.0.22' +managed-jetty = '11.0.23' micronaut-reactor = "3.5.0" micronaut-security = "4.10.0" From 9416c2dca17ef196258fba0de04fa8f5c43e522c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:27:19 -0400 Subject: [PATCH 150/180] Update dependency io.undertow:undertow-servlet to v2.3.17.Final (#787) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3bfe91cc..6f0f9b4f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' -undertow = '2.3.15.Final' +undertow = '2.3.17.Final' tomcat = '10.1.28' bcpkix = "1.70" From e9a366efa47dfb7f1dee76671d99b6f47d6bba12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:27:35 -0400 Subject: [PATCH 151/180] Update plugin io.micronaut.build.shared.settings to v7.2.1 (#786) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 0fe362eff..5234307ff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.2.0' + id 'io.micronaut.build.shared.settings' version '7.2.1' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 5e470d0a0a5a0e6958475b0f20d63e7b63ea5b34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:18:10 -0400 Subject: [PATCH 152/180] Update managed.jetty to v11.0.24 (#792) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f0f9b4f4..09decf98c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ tomcat = '10.1.28' bcpkix = "1.70" managed-apache-http-core5 = "5.2.5" -managed-jetty = '11.0.23' +managed-jetty = '11.0.24' micronaut-reactor = "3.5.0" micronaut-security = "4.10.0" From 8b87a13efe2949609558b8c5870514c6ac46fc98 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 3 Sep 2024 22:19:10 +0000 Subject: [PATCH 153/180] [skip ci] Release v4.11.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 457bcd0aa..8f891275c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.1-SNAPSHOT +projectVersion=4.11.1 projectGroup=io.micronaut.servlet title=Micronaut Servlet From 6649598933abc0ae57a8ee9c8d3d38baf3f03992 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 3 Sep 2024 22:23:23 +0000 Subject: [PATCH 154/180] chore: Bump version to 4.11.2-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8f891275c..53ddc92f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.1 +projectVersion=4.11.2-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From b1a0293d06dc8e1c237c63c51f89ff738b1dc371 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:41:55 -0400 Subject: [PATCH 155/180] Update dependency io.micronaut:micronaut-core-bom to v4.6.4 (#793) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09decf98c..7796b8737 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.6.3" +micronaut = "4.6.4" micronaut-docs = "2.0.0" micronaut-test = "4.5.0" From 24d39dc9a907382cb8ef19e5e78c4d7b549cff3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:23:06 +0000 Subject: [PATCH 156/180] Update dependency gradle to v8.10.2 (#794) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b4155..df97d72b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 3b27d50d64a8bb3e9e5d28170136ae08eeeaf013 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:22:39 +0000 Subject: [PATCH 157/180] Update dependency io.micronaut.gradle:micronaut-gradle-plugin to v4.4.3 (#801) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7796b8737..d4ea17b38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ kotlin = "1.9.25" micronaut-logging = "1.4.0" # Micronaut -micronaut-gradle-plugin = "4.4.2" +micronaut-gradle-plugin = "4.4.3" [libraries] # Core From fe362e09c8d6aba187ee56a48d4f1e5c1f703bce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:03:25 +0000 Subject: [PATCH 158/180] Update dependency io.micronaut.security:micronaut-security-bom to v4.10.2 (#795) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4ea17b38..7476ddbc9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ managed-apache-http-core5 = "5.2.5" managed-jetty = '11.0.24' micronaut-reactor = "3.5.0" -micronaut-security = "4.10.0" +micronaut-security = "4.10.2" micronaut-serde = "2.11.0" micronaut-session = "4.4.0" micronaut-validation = "4.7.0" From 509e9708e0b5f1406cc0ea97e47920939e16743e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:22:34 +0000 Subject: [PATCH 159/180] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.6.6 (#796) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7476ddbc9..495a6bc45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.6.4" +micronaut = "4.6.6" micronaut-docs = "2.0.0" micronaut-test = "4.5.0" From 2dfa0bdf143135d91b19fdb6f2bf5c23669bf6f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 01:51:08 +0000 Subject: [PATCH 160/180] fix(deps): update dependency org.apache.tomcat.embed:tomcat-embed-core to v10.1.30 (#797) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 495a6bc45..19c3353ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' undertow = '2.3.17.Final' -tomcat = '10.1.28' +tomcat = '10.1.30' bcpkix = "1.70" managed-apache-http-core5 = "5.2.5" From 09c57373ada02caca3a58473d5f3af915654275b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:54:36 +0000 Subject: [PATCH 161/180] chore(deps): update graalvm/setup-graalvm action to v1.2.4 (#805) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index be653308d..d7daa1719 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,7 +45,7 @@ jobs: fetch-depth: 0 - name: "🔧 Setup GraalVM CE" - uses: graalvm/setup-graalvm@v1.2.3 + uses: graalvm/setup-graalvm@v1.2.4 with: distribution: 'graalvm' java-version: ${{ matrix.java }} From 23302a09d561e1d7bfa6a6ca47ec18dc0ce52e5a Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 16 Oct 2024 14:56:32 -0400 Subject: [PATCH 162/180] Share session input buffer between requests for poja --- .../http/poja/apache/ApacheServerlessApplication.java | 9 ++++++++- .../http/poja/apache/ApacheServletHttpRequest.java | 8 +++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java index f4c291609..d8bf9e31a 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java @@ -34,7 +34,9 @@ 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.SessionInputBufferImpl; import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl; +import org.apache.hc.core5.http.io.SessionInputBuffer; import org.apache.hc.core5.http.io.SessionOutputBuffer; import java.io.IOException; @@ -57,6 +59,7 @@ public class ApacheServerlessApplication private final ExecutorService ioExecutor; private final ByteBufferFactory byteBufferFactory; private final ApacheServletConfiguration configuration; + private SessionInputBuffer sessionInputBuffer; /** * Default constructor. @@ -82,8 +85,12 @@ protected void handleSingleRequest( ) throws IOException { ApacheServletHttpResponse response = new ApacheServletHttpResponse<>(conversionService); try { + // The buffer is initialized only once + if (sessionInputBuffer == null) { + sessionInputBuffer = new SessionInputBufferImpl(configuration.inputBufferSize()); + } ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>( - in, conversionService, codecRegistry, ioExecutor, byteBufferFactory, response, configuration + in, sessionInputBuffer, conversionService, codecRegistry, ioExecutor, byteBufferFactory, response, configuration ); servletHttpHandler.service(exchange); } catch (ApacheServletBadRequestException e) { diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java index 7cec58f3a..bc6091da6 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java @@ -42,7 +42,7 @@ import org.apache.hc.core5.http.impl.io.ChunkedInputStream; import org.apache.hc.core5.http.impl.io.ContentLengthInputStream; import org.apache.hc.core5.http.impl.io.DefaultHttpRequestParser; -import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; +import org.apache.hc.core5.http.io.SessionInputBuffer; import org.apache.hc.core5.http.io.entity.EmptyInputStream; import org.apache.hc.core5.net.URIBuilder; @@ -94,6 +94,7 @@ public final class ApacheServletHttpRequest extends PojaHttpRequest 0) { bodyStream = new ContentLengthInputStream(sessionInputBuffer, inputStream, contentLength); From 9318faefd6a645ae82b8a111c9b83493687e261b Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 16 Oct 2024 15:03:30 -0400 Subject: [PATCH 163/180] Improve exception handling --- .../io/micronaut/http/poja/PojaHttpServerlessApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 603023914..e7461253e 100644 --- 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 @@ -112,7 +112,7 @@ protected ServletExchange createExchange(Object request, Object respon return start(System.in, System.out); } } catch (IOException e) { - throw new RuntimeException(); + throw new RuntimeException(e); } } From bec0dcca2efa811ccd8a21e22da34f3f6d2784b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:53:25 +0000 Subject: [PATCH 164/180] fix(deps): update dependency io.micronaut.serde:micronaut-serde-bom to v2.11.1 (#807) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19c3353ae..346e1b797 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ managed-jetty = '11.0.24' micronaut-reactor = "3.5.0" micronaut-security = "4.10.2" -micronaut-serde = "2.11.0" +micronaut-serde = "2.11.1" micronaut-session = "4.4.0" micronaut-validation = "4.7.0" google-cloud-functions = '1.1.0' From 5736bc28be01cbef6ee46a44ae6f3106e75f0616 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 17 Oct 2024 12:52:50 -0400 Subject: [PATCH 165/180] Stop the POJA application if input stream is closed --- .../poja/apache/ApacheServletHttpRequest.java | 4 +++ .../poja/PojaHttpServerlessApplication.java | 4 +++ .../exception/NoPojaRequestException.java | 32 +++++++++++++++++++ test-sample-poja/README.md | 5 +++ test-sample-poja/build.gradle | 1 + .../http/poja/sample/Application.java | 4 ++- test-sample-poja/test-request.txt | 3 ++ 7 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 http-poja-common/src/main/java/io/micronaut/http/poja/exception/NoPojaRequestException.java create mode 100644 test-sample-poja/test-request.txt diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java index bc6091da6..e82bfb1cd 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServletHttpRequest.java @@ -31,6 +31,7 @@ import io.micronaut.http.cookie.Cookies; import io.micronaut.http.poja.PojaHttpRequest; import io.micronaut.http.poja.apache.exception.ApacheServletBadRequestException; +import io.micronaut.http.poja.exception.NoPojaRequestException; import io.micronaut.http.poja.util.MultiValueHeaders; import io.micronaut.http.poja.util.MultiValuesQueryParameters; import io.micronaut.http.simple.cookies.SimpleCookies; @@ -110,6 +111,9 @@ public ApacheServletHttpRequest( } catch (HttpException | IOException e) { throw new ApacheServletBadRequestException("HTTP request could not be parsed", e); } + if (request == null) { + throw new NoPojaRequestException(); + } method = HttpMethod.parse(request.getMethod()); try { 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 index e7461253e..7988a2e0a 100644 --- 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 @@ -17,6 +17,7 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.poja.exception.NoPojaRequestException; import io.micronaut.runtime.ApplicationConfiguration; import io.micronaut.runtime.EmbeddedApplication; import io.micronaut.servlet.http.ServletExchange; @@ -90,6 +91,9 @@ protected ServletExchange createExchange(Object request, Object respon runIndefinitely(servletHttpHandler, input, output); } catch (IOException e) { throw new RuntimeException(e); + } catch (NoPojaRequestException e) { + this.stop(); + Thread.currentThread().interrupt(); } return this; } diff --git a/http-poja-common/src/main/java/io/micronaut/http/poja/exception/NoPojaRequestException.java b/http-poja-common/src/main/java/io/micronaut/http/poja/exception/NoPojaRequestException.java new file mode 100644 index 000000000..b4b3f5775 --- /dev/null +++ b/http-poja-common/src/main/java/io/micronaut/http/poja/exception/NoPojaRequestException.java @@ -0,0 +1,32 @@ +/* + * 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.exception; + +/** + * An exception to be thrown when no additional requests can be parsed. + * This can happen if the input stream is closed when waiting for a new request. + * This is not an error condition, therefore application can simply stop. + */ +public class NoPojaRequestException extends RuntimeException { + + /** + * Constructor for the exception. + */ + public NoPojaRequestException() { + super("No new request was found in the input stream"); + } + +} diff --git a/test-sample-poja/README.md b/test-sample-poja/README.md index 9d57e1c32..7d08ba1bb 100644 --- a/test-sample-poja/README.md +++ b/test-sample-poja/README.md @@ -37,3 +37,8 @@ Hello, Micronaut Without Netty! ``` +Alternatively redirect the input from a file: +```shell +gradle :micronaut-test-sample-poja:run --console=plain Date: Thu, 17 Oct 2024 13:35:27 -0400 Subject: [PATCH 166/180] Fix test --- .../apache/ApacheServerlessApplication.java | 2 ++ .../poja/SimpleServerFailedParseSpec.groovy | 32 +++++++++++++++++++ .../http/poja/SimpleServerSpec.groovy | 19 +---------- .../http/poja/sample/SimpleServerTest.java | 2 +- 4 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerFailedParseSpec.groovy diff --git a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java index d8bf9e31a..20748207c 100644 --- a/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java +++ b/http-poja-apache/src/main/java/io/micronaut/http/poja/apache/ApacheServerlessApplication.java @@ -97,6 +97,8 @@ protected void handleSingleRequest( response.status(HttpStatus.BAD_REQUEST); response.contentType(MediaType.TEXT_PLAIN_TYPE); response.getOutputStream().write(e.getMessage().getBytes()); + writeResponse(response.getNativeResponse(), out); + throw e; } writeResponse(response.getNativeResponse(), out); } diff --git a/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerFailedParseSpec.groovy b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerFailedParseSpec.groovy new file mode 100644 index 000000000..52da6a12d --- /dev/null +++ b/http-poja-apache/src/test/groovy/io/micronaut/http/poja/SimpleServerFailedParseSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.http.poja + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification +import spock.lang.Stepwise + +@Stepwise +@MicronautTest +class SimpleServerFailedParseSpec extends Specification { + + @Inject + SimpleServerSpec.StringTestingClient client + + void "test non-parseable GET method"() { + when: + var response = client.exchange(SimpleServerSpec.unindent(""" + GET /test HTTP/1.1error\r + Host: h\r + \r + """)) + + then: + response == SimpleServerSpec.unindent(""" + HTTP/1.1 400 Bad Request\r + Content-Length: 32\r + Content-Type: text/plain\r + \r + HTTP request could not be parsed""") + } + +} 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 index 0eea38030..19bd95e71 100644 --- 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 @@ -54,23 +54,6 @@ class SimpleServerSpec extends Specification { {"_links":{"self":[{"href":"/invalid-test","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}""") } - void "test non-parseable GET method"() { - when: - var response = client.exchange(unindent(""" - GET /test HTTP/1.1error\r - Host: h\r - \r - """)) - - then: - response == unindent(""" - HTTP/1.1 400 Bad Request\r - Content-Length: 32\r - Content-Type: text/plain\r - \r - HTTP request could not be parsed""") - } - void "test DELETE method"() { when: var response = client.exchange(unindent(""" @@ -153,7 +136,7 @@ class SimpleServerSpec extends Specification { } - private String unindent(String value, int indentSpaces = 8) { + static String unindent(String value, int indentSpaces = 8) { while (value.charAt(0) == '\n' as char) { value = value.substring(1) } diff --git a/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java index df6038f23..eaa43501c 100644 --- a/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java +++ b/test-sample-poja/src/test/java/io/micronaut/http/poja/sample/SimpleServerTest.java @@ -19,7 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @MicronautTest -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@TestMethodOrder(MethodOrderer.MethodName.class) public class SimpleServerTest { @Inject From 4a89cbe87fb4c0267642db6ef0fca586ae44d81d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:25:01 +0000 Subject: [PATCH 167/180] fix(deps): update dependency org.apache.tomcat.embed:tomcat-embed-core to v10.1.31 (#802) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 346e1b797..6f3feb2b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' undertow = '2.3.17.Final' -tomcat = '10.1.30' +tomcat = '10.1.31' bcpkix = "1.70" managed-apache-http-core5 = "5.2.5" From e6f0016519a76f8bd7782985c4a47082e09a5f59 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:09:13 +0000 Subject: [PATCH 168/180] fix(deps): update dependency io.undertow:undertow-servlet to v2.3.18.final (#808) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f3feb2b0..5dee8ff2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ spock = "2.3-groovy-4.0" managed-servlet-api = '6.1.0' kotest-runner = '5.9.1' -undertow = '2.3.17.Final' +undertow = '2.3.18.Final' tomcat = '10.1.31' bcpkix = "1.70" From 55b11670ff79e2d821d35692687e03329f1bebfa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:02:49 +0000 Subject: [PATCH 169/180] chore(deps): update plugin io.micronaut.build.shared.settings to v7.2.2 (#809) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 5234307ff..f82a64a27 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.2.1' + id 'io.micronaut.build.shared.settings' version '7.2.2' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 674c4b3c662fe5eae5ed4aa52da9834fbe686169 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Fri, 25 Oct 2024 19:45:45 +0000 Subject: [PATCH 170/180] [skip ci] Release v.11.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 53ddc92f6..14dc6034a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.2-SNAPSHOT +projectVersion=.11.2 projectGroup=io.micronaut.servlet title=Micronaut Servlet From b6f83beaa8431c3808ee60c3e23563839e62fcbe Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 28 Oct 2024 14:40:04 +0000 Subject: [PATCH 171/180] [skip ci] Release v4.11.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 14dc6034a..123ea899f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=.11.2 +projectVersion=4.11.2 projectGroup=io.micronaut.servlet title=Micronaut Servlet From c2570bfe68e4b15a1f733eb03bf458c54a3eb68b Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 28 Oct 2024 14:44:25 +0000 Subject: [PATCH 172/180] chore: Bump version to 4.11.3-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 123ea899f..596b661db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.2 +projectVersion=4.11.3-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From a9aecb61f275b80daccfcdb2044a9d1f637992db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:38:16 +0000 Subject: [PATCH 173/180] chore(deps): update plugin io.micronaut.build.shared.settings to v7.2.3 (#813) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index f82a64a27..49da268e1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.micronaut.build.shared.settings' version '7.2.2' + id 'io.micronaut.build.shared.settings' version '7.2.3' } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From bfd8ff648d5d9dc062f430c2da01cf309025e41b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:16:18 +0000 Subject: [PATCH 174/180] fix(deps): update dependency io.micronaut.serde:micronaut-serde-bom to v2.11.2 (#814) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5dee8ff2d..d66e7e34e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ managed-jetty = '11.0.24' micronaut-reactor = "3.5.0" micronaut-security = "4.10.2" -micronaut-serde = "2.11.1" +micronaut-serde = "2.11.2" micronaut-session = "4.4.0" micronaut-validation = "4.7.0" google-cloud-functions = '1.1.0' From b54b5ceab4929b40f91503fa81c149bf58ee6884 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 02:03:32 +0000 Subject: [PATCH 175/180] chore(deps): update graalvm/setup-graalvm action to v1.2.5 (#815) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d7daa1719..140d2d617 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,7 +45,7 @@ jobs: fetch-depth: 0 - name: "🔧 Setup GraalVM CE" - uses: graalvm/setup-graalvm@v1.2.4 + uses: graalvm/setup-graalvm@v1.2.5 with: distribution: 'graalvm' java-version: ${{ matrix.java }} From fd2e5a17c376bc5a3213700b30f2e619c0ca8cdc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 31 Oct 2024 11:53:39 +0100 Subject: [PATCH 176/180] ci: projectVersion=4.12.0-SNAPSHOT [ci skip] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 596b661db..fe4063e18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.11.3-SNAPSHOT +projectVersion=4.12.0-SNAPSHOT projectGroup=io.micronaut.servlet title=Micronaut Servlet From 5fa410a9efc0eda8572e47211d4af2d3e7a1a204 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:54:19 +0100 Subject: [PATCH 177/180] chore(deps): update actions/checkout action to v4.2.2 (#799) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4bfd84ac..328380b97 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -146,7 +146,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download artifacts uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: From 92a951e4142a1915450a623653a368d4e777d750 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:54:47 +0100 Subject: [PATCH 178/180] chore(deps): update actions/upload-artifact action to v4.4.3 (#791) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 140d2d617..82d258ec5 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -78,7 +78,7 @@ jobs: - name: "📜 Upload binary compatibility check results" if: matrix.java == '17' - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 328380b97..bc2ac6483 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,13 +66,13 @@ jobs: # Store the hash in a file, which is uploaded as a workflow artifact. sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256 - name: Upload build artifacts - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: gradle-build-outputs path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* retention-days: 5 - name: Upload artifacts-sha256 - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: artifacts-sha256 path: artifacts-sha256 From 43b848696b75195bfe96d6a8102f3f06db79094c Mon Sep 17 00:00:00 2001 From: micronaut-build <65172877+micronaut-build@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:56:17 +0100 Subject: [PATCH 179/180] [servlet] Update common files for branch 4.11.x (#785) * Update common files * Update suppressions.xml * Update .github/workflows/gradle.yml --------- Co-authored-by: Sergio del Amo From 9f1b0ba927b11385cc56eb650968ffbd72cfbdea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:11:42 +0100 Subject: [PATCH 180/180] fix(deps): update dependency org.apache.httpcomponents.core5:httpcore5 to v5.3.1 (#798) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d66e7e34e..e819da99e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ undertow = '2.3.18.Final' tomcat = '10.1.31' bcpkix = "1.70" -managed-apache-http-core5 = "5.2.5" +managed-apache-http-core5 = "5.3.1" managed-jetty = '11.0.24' micronaut-reactor = "3.5.0"