diff --git a/docs-internal/http-features.md b/docs-internal/http-features.md index 09327b9c890..61c22338833 100644 --- a/docs-internal/http-features.md +++ b/docs-internal/http-features.md @@ -8,7 +8,7 @@ Features | Context | 1100 | | Access Log | 1000 | | Tracing | 900 | -| CORS | 950 | +| CORS | 850 | | Security | 800 | | Routing (all handlers) | 100 | | OpenAPI | 90 | diff --git a/docs/src/main/asciidoc/se/webserver.adoc b/docs/src/main/asciidoc/se/webserver.adoc index 42381b7dbc4..e83d9ca6490 100644 --- a/docs/src/main/asciidoc/se/webserver.adoc +++ b/docs/src/main/asciidoc/se/webserver.adoc @@ -32,10 +32,14 @@ include::{rootdir}/includes/se.adoc[] - <> ** <> ** <> +** <> ** <> *** <> *** <> *** <> +- <> +** <> +** <> - <> ** <> ** <> @@ -96,6 +100,85 @@ include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_2, indent=0] just use `Config.create()` <2> Server expects the configuration tree located on the node of `server` +=== Configuring TLS + +Configure TLS either programmatically, or by the Helidon configuration framework. + +==== Configuring TLS in Your Code + +To configure TLS in WebServer programmatically create your keystore configuration and pass it to the WebServer builder. + +[source,java] +---- +include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_30, indent=0] +---- + + +==== Configuring TLS in the Config File + +It is also possible to configure TLS via the config file. + +[source,yaml] +.WebServer TLS configuration file `application.yaml` +---- +server: + tls: + #Truststore setup + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "keystore.p12" + # Keystore with private key and server certificate + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "keystore.p12" +---- +Then, in your application code, load the configuration from that file. + +[source,java] +.WebServer initialization using the `application.yaml` file located on the classpath +---- +include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_2, indent=0] +---- +<1> `application.yaml` is a default configuration source loaded when YAML support is on classpath, so we can +just use `Config.create()` +<2> Server expects the configuration tree located on the node of `server` + +Or you can only create WebServerTls instance based on the config file. + +[source,java] +.WebServerTls instance based on `application.yaml` file located on the classpath +---- +include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_31, indent=0] +---- + +This can alternatively be configured with paths to PKCS#8 PEM files rather than KeyStores: + +[source,yaml] +.WebServer TLS configuration file `application.yaml` +---- +server: + tls: + #Truststore setup + trust: + pem: + certificates: + resource: + resource-path: "ca-bundle.pem" + private-key: + pem: + key: + resource: + resource-path: "key.pem" + cert-chain: + resource: + resource-path: "chain.pem" +---- + === Configuration Options include::{rootdir}/config/io_helidon_webserver_WebServer.adoc[leveloffset=+2,tag=config] @@ -202,6 +285,15 @@ include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_7, indent=0] ---- In this example, the `GET` handler matches requests to `/hello/subpath`. +[[anchor-http-feature]] +=== Using `HttpFeature` + +By implementing the `io.helidon.webserver.http.HttpFeature` interface, you can organize multiple routes and/or filters into +a feature, that will be setup according to its defined `io.helidon.common.Weight` (or using `io.helidon.common.Weighted`). + +Each service has access to the routing builder. HTTP Features are configured for each routing builder. If there is a need +to configure a feature for multiple sockets, you can use <> instead. + == Request Handling Implement the logic to handle requests to WebServer in a `Handler`, which is a `FunctionalInterface`. @@ -389,6 +481,126 @@ include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_21, indent=0] * Otherwise, the exceptions are translated to an Internal Server Error HTTP error code `500`. +=== Configuration Options + +include::{rootdir}/config/io_helidon_common_tls_Tls.adoc[leveloffset=+2,tag=config] + +== Server Features +Server features provide additional functionality to the WebServer, through modification of the server configuration, +listener configuration, or routing. + +A server feature can be added by implementing `io.helidon.webserver.spi.ServerFeature`. +Server features support automated discovery, as long as the implementation is available through Java `ServiceLoader`. +Server features can also be added through configuration, as can be seen above in <>, +configuration key `features`. + +All features (both `ServerFeature` and <>) honor weight of the feature +(defined either through `@Weight` annotation, or by implementing `Weighted` interface) when registering routes, +`HttpService`, or `Filter` to the routing. + +The following table shows available server features and their weight. The highest weight is always registered (and invoked) +first. + + +|=== +|Feature |Weight + +|<> +|1100 + +|<> +|1000 + +|xref:tracing.adoc[Tracing] +|900 + +|xref:cors.adoc[CORS] +|850 + +|xref:security/introduction.adoc[Security] +|800 + +|Routing (all handlers and filters) +|100 + +|xref:openapi/openapi.adoc[OpenAPI] +|90 + +|xref:observability.adoc[Observability] +|80 +|=== + +=== Context + +Context feature adds a filter that executes all requests within the context of `io.helidon.common.context.Context`. +A `Context` instance is available on `ServerRequest` even if this feature is not added. This feature adds support for +obtaining request context through `io.helidon.common.context.Contexts.context()`. + +This feature will provide the same behavior as previous versions of Helidon. Since Helidon 4.0.0, this feature is not +automatically added. + +To enable execution of routes within Context, add the following dependency to project's `pom.xml`: + +[source,xml] +---- + + io.helidon.webserver + helidon-webserver-context + +---- + +Context feature can be configured, all options shown below are also available both in config, and programmatically +when using builder. + +include::{rootdir}/config/io_helidon_webserver_context_ContextFeature.adoc[leveloffset=+1] + +=== Access Log + +Access logging in Helidon is done by a dedicated module that can be +added to WebServer and configured. + +Access logging is a Helidon WebServer `ServerFeature`. Access Log feature has a +very high weight, so it is registered before other features (such as security) that may +terminate a request. This is to ensure the log contains all requests with appropriate status codes. + +To enable Access logging add the following dependency to project's `pom.xml`: + +[source,xml] +---- + + io.helidon.webserver + helidon-webserver-access-log + +---- + +==== Configuring Access Log in Your Code + +`AccessLogFeature` is discovered automatically by default, and configured through `server.features.access-log`. +You can also configure this feature in code by registering it with WebServer (which will replace the discovered feature). + +[source,java] +---- +include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_29, indent=0] +---- + +==== Configuring Access Log in a Configuration File + +Access log can be configured as follows: + +[source, yaml] +.Access Log configuration file +---- +server: + port: 8080 + features: + access-log: + format: "%h %l %u %t %r %s %b %{Referer}i" +---- + +All options shown below are also available programmatically when using builder. + +include::{rootdir}/config/io_helidon_webserver_accesslog_AccessLogConfig.adoc[leveloffset=+1] + == Supported Technologies == HTTP/2 Support @@ -624,138 +836,6 @@ curl --noproxy '*' -X POST -H "Content-Type: application/json" \ {"name":"Joe"} ---- -== Access Log - -Access logging in Helidon is done by a dedicated module that can be -added to WebServer and configured. - -Access logging is a Helidon WebServer `ServerFeature`. Access Log feature has a -very high weight, so it is registered before other features (such as security) that may -terminate a request. This is to ensure the log contains all requests with appropriate status codes. - -To enable Access logging add the following dependency to project's `pom.xml`: - -[source,xml] ----- - - io.helidon.webserver - helidon-webserver-access-log - ----- - - -=== Configuring Access Log in Your Code - -`AccessLogFeature` is discovered automatically by default, and configured through `server.features.access-log`. -You can also configure this feature in code by registering it with WebServer (which will replace the discovered feature). - -[source,java] ----- -include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_29, indent=0] ----- - -=== Configuring Access Log in a Configuration File - -Access log can be configured as follows: - -[source, yaml] -.Access Log configuration file ----- -server: - port: 8080 - features: - access-log: - format: "%h %l %u %t %r %s %b %{Referer}i" ----- - -All options shown below are also available programmatically when using builder. - -include::{rootdir}/config/io_helidon_webserver_accesslog_AccessLogConfig.adoc[leveloffset=+1] - -== TLS Configuration - -Configure TLS either programmatically, or by the Helidon configuration framework. - -=== Configuring TLS in Your Code - -To configure TLS in WebServer programmatically create your keystore configuration and pass it to the WebServer builder. - -[source,java] ----- -include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_30, indent=0] ----- - - -=== Configuring TLS in the Config File - -It is also possible to configure TLS via the config file. - -[source,yaml] -.WebServer TLS configuration file `application.yaml` ----- -server: - tls: - #Truststore setup - trust: - keystore: - passphrase: "password" - trust-store: true - resource: - resource-path: "keystore.p12" - # Keystore with private key and server certificate - private-key: - keystore: - passphrase: "password" - resource: - resource-path: "keystore.p12" ----- -Then, in your application code, load the configuration from that file. - -[source,java] -.WebServer initialization using the `application.yaml` file located on the classpath ----- -include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_2, indent=0] ----- -<1> `application.yaml` is a default configuration source loaded when YAML support is on classpath, so we can -just use `Config.create()` -<2> Server expects the configuration tree located on the node of `server` - -Or you can only create WebServerTls instance based on the config file. - -[source,java] -.WebServerTls instance based on `application.yaml` file located on the classpath ----- -include::{sourcedir}/se/WebServerSnippets.java[tag=snippet_31, indent=0] ----- - -This can alternatively be configured with paths to PKCS#8 PEM files rather than KeyStores: - -[source,yaml] -.WebServer TLS configuration file `application.yaml` ----- -server: - tls: - #Truststore setup - trust: - pem: - certificates: - resource: - resource-path: "ca-bundle.pem" - private-key: - pem: - key: - resource: - resource-path: "key.pem" - cert-chain: - resource: - resource-path: "chain.pem" ----- - -=== Configuration Options - -include::{rootdir}/config/io_helidon_common_tls_Tls.adoc[leveloffset=+2,tag=config] - - == HTTP Content Encoding HTTP encoding can improve bandwidth utilization and transfer speeds in certain scenarios. It diff --git a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsFeature.java b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsFeature.java index 22e70d25f5b..ccfdf92e228 100644 --- a/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsFeature.java +++ b/webserver/cors/src/main/java/io/helidon/webserver/cors/CorsFeature.java @@ -34,7 +34,7 @@ public class CorsFeature implements Weighted, ServerFeature, RuntimeType.Api res.send("routing")); + updateRouting(routing, "routing-1", 0, FIRST); + updateRouting(routing, "routing-2", 0, SECOND); + } + + @Test + void testServiceRegistration() { + assertThat(client.get("/service") + .requestEntity(String.class), is("service:server-feature:1000")); + } + + @Test + void testServiceWithPathRegistration() { + assertThat(client.get("/path/service") + .requestEntity(String.class), is("service:server-feature:1000")); + } + + @Test + void testRouteRegistration() { + assertThat(client.get("/route") + .requestEntity(String.class), is("route:server-feature:1000")); + } + + @Test + void testErrorRegistration() { + assertThat(client.get("/error") + .requestEntity(String.class), is("error:server-feature:1000")); + } + + @Test + void testFilterRegistration() { + List filters = new CopyOnWriteArrayList<>(); + FIRST.filters(filters); + SECOND.filters(filters); + F_FILTER_1000.filters(filters); + F_FILTER_99.filters(filters); + HF_FILTER_999.filters(filters); + HF_FILTER_98.filters(filters); + + assertThat(client.get("/no-service") + .requestEntity(String.class), + is("routing")); + + /* + server-feature(1000) + http-feature(999) + routing-first(N/A) + routing-second(N/A) + server-feature(99) + http-feature(98) + */ + + /* + now the order of filers should be as follows: + F_FILTER_1000 - highest weight + HF_FILTER_999 + FIRST - default weight (registered through routing) + SECOND - dtto + F_FILTER_99 - lower than default, should be handled after default routing + HF_FILTER_98 + */ + assertThat("All 6 filters should have been called. Actual list: " + filters, filters.size(), is(6)); + assertThat("First should be filter from server feature, weight 1000. Actual list: " + filters, + filters.get(0), + sameInstance(F_FILTER_1000)); + assertThat("Second should be filter from HTTP feature, weight 999. Actual list: " + filters, + filters.get(1), + sameInstance(HF_FILTER_999)); + assertThat("Third should be first filter registered to routing. Actual list: " + filters, + filters.get(2), + sameInstance(FIRST)); + assertThat("Fourth should be second filter registered to routing. Actual list: " + filters, + filters.get(3), + sameInstance(SECOND)); + assertThat("Fifth should be filter from server feature, weight 99. Actual list: " + filters, + filters.get(4), + sameInstance(F_FILTER_99)); + assertThat("Last should be filter from HTTP feature, weight 98. Actual list: " + filters, + filters.get(5), + sameInstance(HF_FILTER_98)); + + } + + private static void updateRouting(HttpRouting.Builder routing, String type, int weight, Filter filter) { + routing.addFilter(filter) + .register(new TestHttpService(type, weight)) + .register("/path", new TestHttpService(type, weight)) + .route(HttpRoute.builder() + .path("/route") + .handler((req, res) -> res.send("route:" + type + ":" + weight)) + .build()) + .get("/error", (req, res) -> { + throw new TestException(type, weight); + }) + .error(TestException.class, (req, res, throwable) -> { + res.send("error:" + throwable.type + ":" + throwable.weight); + }); + } + + private static class TestHttpService implements HttpService { + private final String type; + private final int weight; + + private TestHttpService(String type, int weight) { + this.type = type; + this.weight = weight; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/service", (req, res) -> res.send("service:" + type + ":" + weight)); + } + } + + private static class TestException extends RuntimeException { + private final String type; + private final int weight; + + private TestException(String type, int weight) { + this.type = type; + this.weight = weight; + } + } + + private static class TestFilter implements Filter { + private final String message; + private final int weight; + + private volatile List filters; + + private TestFilter(String message, int weight) { + this.message = message; + this.weight = weight; + } + + @Override + public void filter(FilterChain filterChain, RoutingRequest routingRequest, RoutingResponse routingResponse) { + if (filters != null) { + filters.add(this); + } + filterChain.proceed(); + } + + public void filters(List filters) { + this.filters = filters; + } + + @Override + public String toString() { + return message + "(" + weight + ")"; + } + } + + private static class TestHttpFeature implements HttpFeature, Weighted { + private final int weight; + private final TestFilter filter; + + private TestHttpFeature(int weight) { + this.weight = weight; + this.filter = new TestFilter("http-feature", weight); + } + + @Override + public void setup(HttpRouting.Builder routing) { + updateRouting(routing, "http-feature", weight, filter); + } + + @Override + public double weight() { + return weight; + } + + @Override + public String toString() { + return "http-feature(" + weight + ")"; + } + + TestFilter filter() { + return filter; + } + } + + private static class TestServerFeature implements ServerFeature, Weighted { + private final int weight; + private final TestFilter filter; + + private TestServerFeature(int weight) { + this.weight = weight; + this.filter = new TestFilter("server-feature", weight); + } + + @Override + public void setup(ServerFeatureContext featureContext) { + updateRouting(featureContext.socket(WebServer.DEFAULT_SOCKET_NAME) + .httpRouting(), "server-feature", weight, filter); + } + + @Override + public String name() { + return toString(); + } + + @Override + public String type() { + return toString(); + } + + @Override + public double weight() { + return weight; + } + + @Override + public String toString() { + return "server-feature(" + weight + ")"; + } + + TestFilter filter() { + return filter; + } + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java b/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java index cd8ce1c53fd..a9d0bebf800 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java @@ -17,6 +17,7 @@ package io.helidon.webserver; import java.lang.management.ManagementFactory; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -35,7 +36,6 @@ import io.helidon.Main; import io.helidon.common.SerializationConfig; import io.helidon.common.Version; -import io.helidon.common.Weighted; import io.helidon.common.Weights; import io.helidon.common.context.Context; import io.helidon.common.features.HelidonFeatures; @@ -77,13 +77,13 @@ class LoomServer implements WebServer { sockets.put(DEFAULT_SOCKET_NAME, serverConfig); // features ordered by weight - List features = serverConfig.features(); + List features = new ArrayList<>(serverConfig.features()); + Weights.sort(features); + ServerFeatureContextImpl featureContext = ServerFeatureContextImpl.create(serverConfig); for (ServerFeature feature : features) { - featureContext.weight(Weights.find(feature, Weighted.DEFAULT_WEIGHT)); - feature.setup(featureContext); + featureContext.setUpFeature(feature); } - featureContext.weight(Weighted.DEFAULT_WEIGHT); Timer idleConnectionTimer = new Timer("helidon-idle-connection-timer", true); Map listenerMap = new HashMap<>(); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerFeatureContextImpl.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerFeatureContextImpl.java index 8372cea6fd7..afa0a79f0aa 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ServerFeatureContextImpl.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerFeatureContextImpl.java @@ -48,6 +48,7 @@ class ServerFeatureContextImpl implements ServerFeature.ServerFeatureContext { private final WebServerConfig serverConfig; private final Map socketToBuilders; private final Set configuredSockets; + private final Map inProgressBuilders; private final AtomicReference weight; @@ -61,6 +62,7 @@ private ServerFeatureContextImpl(WebServerConfig serverConfig, .filter(Predicate.not(DEFAULT_SOCKET_NAME::equals)) .collect(Collectors.toSet()); this.weight = weight; + this.inProgressBuilders = new HashMap<>(); } static ServerFeatureContextImpl create(WebServerConfig serverConfig) { @@ -73,7 +75,7 @@ static ServerFeatureContextImpl create(WebServerConfig serverConfig) { Map socketToBuilders = new HashMap<>(); socketToBuilders.put(DEFAULT_SOCKET_NAME, - new ListenerBuildersImpl(DEFAULT_SOCKET_NAME, serverConfig, httpRouting, routings, weight)); + new ListenerBuildersImpl(DEFAULT_SOCKET_NAME, serverConfig, httpRouting, routings)); // for each socket, gather all routing builders sockets.forEach((socketName, listener) -> { @@ -95,7 +97,7 @@ static ServerFeatureContextImpl create(WebServerConfig serverConfig) { builders.add(listenerHttpRouting); } socketToBuilders.put(socketName, - new ListenerBuildersImpl(socketName, serverConfig, listenerHttpRouting, builders, weight)); + new ListenerBuildersImpl(socketName, serverConfig, listenerHttpRouting, builders)); }); return new ServerFeatureContextImpl(serverConfig, socketToBuilders, weight); @@ -117,22 +119,32 @@ public Set sockets() { } @Override - public ListenerBuildersImpl socket(String socketName) { + public ServerFeature.SocketBuilders socket(String socketName) { return Optional.ofNullable(socketToBuilders.get(socketName)) + .map(it -> socketBuilderDelegate(socketName, it)) .orElseThrow(() -> new NoSuchElementException("There is no socket configuration for socket named \"" + socketName + "\"")); } - void weight(double weight) { - this.weight.set(weight); + void setUpFeature(ServerFeature feature) { + weight(Weights.find(feature, Weighted.DEFAULT_WEIGHT)); + /* + We need to create a routing that is specific to each feature, and wrap it as an HttpFeature, to honor weight + for cases, where a route, service, or a filter is added from a feature - it must be in the correct order + This will honor weights across Server features and HTTP features for webserver + */ + feature.setup(this); + inProgressBuilders.forEach(this::createHttpFeature); + inProgressBuilders.clear(); + weight(Weighted.DEFAULT_WEIGHT); } - Router router() { - return router(DEFAULT_SOCKET_NAME); + void weight(double weight) { + this.weight.set(weight); } Router router(String socketName) { - ListenerBuildersImpl listener = socket(socketName); + ListenerBuildersImpl listener = listenerBuilder(socketName); boolean containsHttp = listener.routings.stream() .anyMatch(it -> it instanceof HttpRouting.Builder); @@ -164,6 +176,54 @@ private static HttpRouting.Builder defaultRouting(WebServerConfig serverConfig) return httpRouting; } + private ServerFeature.SocketBuilders socketBuilderDelegate(String socketName, ListenerBuildersImpl listenerBuilders) { + return new ServerFeature.SocketBuilders() { + @Override + public ListenerConfig listener() { + return listenerBuilders.listener(); + } + + @Override + public HttpRouting.Builder httpRouting() { + return inProgressBuilders.computeIfAbsent(socketName, + it -> new ServerToHttpFeatureBuilder(weight.get(), + listenerBuilders.httpRouting())); + } + + @Override + public ServerFeature.RoutingBuilders routingBuilders() { + ServerFeature.RoutingBuilders delegate = listenerBuilders.routingBuilders(); + return new ServerFeature.RoutingBuilders() { + @Override + public boolean hasRouting(Class builderType) { + return delegate.hasRouting(builderType); + } + + @SuppressWarnings("unchecked") + @Override + public > T routingBuilder(Class builderType) { + if (builderType.equals(HttpRouting.class)) { + return (T) httpRouting(); + } + return delegate.routingBuilder(builderType); + } + }; + } + }; + } + + private ListenerBuildersImpl listenerBuilder(String socketName) { + return Optional.ofNullable(socketToBuilders.get(socketName)) + .orElseThrow(() -> new NoSuchElementException("There is no socket configuration for socket named \"" + + socketName + "\"")); + } + + private void createHttpFeature(String socket, ServerToHttpFeatureBuilder builder) { + socket(socket) + .httpRouting() + .addFeature(builder.toFeature()); + } + private static class RoutingBuildersImpl implements ServerFeature.RoutingBuilders { private final String socketName; private final Map, Object> buildersByType; @@ -202,18 +262,15 @@ private static class ListenerBuildersImpl implements ServerFeature.SocketBuilder private final ListenerConfig listenerConfig; private final HttpRouting.Builder httpRouting; private final List> routings; - private final AtomicReference weight; private final ServerFeature.RoutingBuilders routingBuilders; ListenerBuildersImpl(String socketName, ListenerConfig listenerConfig, HttpRouting.Builder httpRouting, - List> routings, - AtomicReference weight) { + List> routings) { this.listenerConfig = listenerConfig; this.httpRouting = httpRouting; this.routings = routings; - this.weight = weight; this.routingBuilders = RoutingBuildersImpl.create(socketName, routings); } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerToHttpFeatureBuilder.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerToHttpFeatureBuilder.java new file mode 100644 index 00000000000..051c16c275b --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerToHttpFeatureBuilder.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 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 + * + * http://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.helidon.webserver; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.common.Weighted; +import io.helidon.webserver.http.ErrorHandler; +import io.helidon.webserver.http.Filter; +import io.helidon.webserver.http.HttpFeature; +import io.helidon.webserver.http.HttpRoute; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.HttpSecurity; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.Registration; + +class ServerToHttpFeatureBuilder implements HttpRouting.Builder { + private final List registrations = new ArrayList<>(); + + private final double weight; + private final HttpRouting.Builder delegate; + + ServerToHttpFeatureBuilder(double weight, HttpRouting.Builder delegate) { + this.weight = weight; + this.delegate = delegate; + } + + @Override + public HttpRouting.Builder register(HttpService... services) { + registrations.add(Registration.create(services)); + return this; + } + + @Override + public HttpRouting.Builder register(String path, HttpService... services) { + registrations.add(Registration.create(path, services)); + return this; + } + + @Override + public HttpRouting.Builder route(HttpRoute route) { + registrations.add(Registration.create(route)); + return this; + } + + @Override + public HttpRouting.Builder addFilter(Filter filter) { + registrations.add(Registration.create(filter)); + return this; + } + + @Override + public HttpRouting.Builder addFeature(Supplier feature) { + // features are always directly sent to delegate, as they have correct ordering based on weight + delegate.addFeature(feature); + return this; + } + + @Override + public HttpRouting.Builder error(Class exceptionClass, ErrorHandler handler) { + registrations.add(Registration.create(exceptionClass, handler)); + return this; + } + + @Override + public HttpRouting.Builder maxReRouteCount(int maxReRouteCount) { + registrations.add(Registration.createMaxRerouteCount(maxReRouteCount)); + return this; + } + + @Override + public HttpRouting.Builder security(HttpSecurity security) { + registrations.add(Registration.create(security)); + return this; + } + + @Override + public HttpRouting.Builder copy() { + ServerToHttpFeatureBuilder copy = new ServerToHttpFeatureBuilder(weight, delegate.copy()); + copy.registrations.addAll(this.registrations); + return copy; + } + + @Override + public HttpRouting build() { + throw new UnsupportedOperationException("This method should never escape internal Helidon types"); + } + + HttpFeature toFeature() { + return new HttpFeatureForServerFeature(this, weight); + } + + private static class HttpFeatureForServerFeature implements HttpFeature, Weighted { + private final ServerToHttpFeatureBuilder builder; + private final double weight; + + private HttpFeatureForServerFeature(ServerToHttpFeatureBuilder builder, double weight) { + this.builder = builder; + this.weight = weight; + } + + @Override + public void setup(HttpRouting.Builder routing) { + builder.registrations.forEach(it -> it.register(routing)); + } + + @Override + public double weight() { + return weight; + } + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/HttpRoutingFeature.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/HttpRoutingFeature.java index 698346ad4bf..275e9a62665 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http/HttpRoutingFeature.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/HttpRoutingFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -37,68 +37,28 @@ public void setup(HttpRouting.Builder routing) { } void filter(Filter filter) { - this.registrations.add(new FilterReg(filter)); + this.registrations.add(Registration.create(filter)); } void error(Class exceptionClass, ErrorHandler handler) { - this.registrations.add(new ErrorReg<>(exceptionClass, handler)); + this.registrations.add(Registration.create(exceptionClass, handler)); } void service(HttpService... services) { - this.registrations.add(new ServiceReg(services)); + this.registrations.add(Registration.create(services)); } void service(String path, HttpService... services) { - this.registrations.add(new ServicePathReg(path, services)); + this.registrations.add(Registration.create(path, services)); } void route(HttpRoute route) { - this.registrations.add(new RouteReg(route)); + this.registrations.add(Registration.create(route)); } void copyFrom(HttpRoutingFeature mainRouting) { this.registrations.addAll(mainRouting.registrations); } - private interface Registration { - void register(HttpRouting.Builder routing); - } - - private record FilterReg(Filter filter) implements Registration { - - @Override - public void register(HttpRouting.Builder routing) { - routing.addFilter(filter); - } - } - - private record ErrorReg(Class exceptionClass, ErrorHandler handler) - implements Registration { - - @Override - public void register(HttpRouting.Builder routing) { - routing.error(exceptionClass, handler); - } - } - - private record ServiceReg(HttpService[] services) implements Registration { - @Override - public void register(HttpRouting.Builder routing) { - routing.register(services); - } - } - private record ServicePathReg(String path, HttpService[] services) implements Registration { - @Override - public void register(HttpRouting.Builder routing) { - routing.register(path, services); - } - } - - private record RouteReg(HttpRoute route) implements Registration { - @Override - public void register(HttpRouting.Builder routing) { - routing.route(route); - } - } } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/Registration.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/Registration.java new file mode 100644 index 00000000000..509e06ab7f1 --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/Registration.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 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 + * + * http://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.helidon.webserver.http; + +/** + * A routing builder registration. + *

+ * This type is used internally in Helidon to allow gathering of registrations, and then re-applying them on a + * different builder. + */ +public interface Registration { + /** + * Create a registration for service(s) with a path. + * + * @param path path of the service(s) + * @param services service(s) to register + * @return a new registration + */ + static Registration create(String path, HttpService... services) { + return new Registrations.ServicePathRegistration(path, services); + } + + /** + * Create a registration for service(s). + * + * @param services service(s) to register + * @return a new registration + */ + static Registration create(HttpService... services) { + return new Registrations.ServiceRegistration(services); + } + + /** + * Create a registration for a route. + * + * @param route to register + * @return a new registration + */ + static Registration create(HttpRoute route) { + return new Registrations.RouteRegistration(route); + } + + /** + * Create a registration for a filter. + * + * @param filter to register + * @return a new registration + */ + static Registration create(Filter filter) { + return new Registrations.FilterRegistration(filter); + } + + /** + * Create a registration for an error handler. + * + * @param exceptionClass class of exception to map this handler to + * @param handler handler to handle that exception + * @param type of the exception to be handled + * @return a new registration + */ + static Registration create(Class exceptionClass, ErrorHandler handler) { + return new Registrations.ErrorRegistration<>(exceptionClass, handler); + } + + /** + * Create a registration for configuration of max re-route count. + * + * @param maxReRouteCount maximal number of re-routes to allow + * @return a new registration + */ + static Registration createMaxRerouteCount(int maxReRouteCount) { + return new Registrations.MaxRerouteCountRegistration(maxReRouteCount); + } + + /** + * Create a registration for HTTP security instance. + * + * @param security to register + * @return a new registration + */ + static Registration create(HttpSecurity security) { + return new Registrations.SecurityRegistration(security); + } + + /** + * Register this registration on a different routing builder. + * + * @param routing the routing builder to apply this registration on + */ + void register(HttpRouting.Builder routing); + +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/Registrations.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/Registrations.java new file mode 100644 index 00000000000..6078a19d965 --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/Registrations.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 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 + * + * http://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.helidon.webserver.http; + +class Registrations { + static final class ServicePathRegistration implements Registration { + private final HttpService[] service; + private final String path; + + ServicePathRegistration(String path, HttpService... service) { + this.path = path; + this.service = service; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.register(path, service); + } + } + + static final class ServiceRegistration implements Registration { + private final HttpService[] service; + + ServiceRegistration(HttpService... service) { + this.service = service; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.register(service); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + static final class ErrorRegistration implements Registration { + private final Class exceptionClass; + private final ErrorHandler handler; + + ErrorRegistration(Class exceptionClass, ErrorHandler handler) { + this.exceptionClass = exceptionClass; + this.handler = handler; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.error(exceptionClass, handler); + } + } + + static final class RouteRegistration implements Registration { + private final HttpRoute route; + + RouteRegistration(HttpRoute route) { + this.route = route; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.route(route); + } + } + + static final class FilterRegistration implements Registration { + private final Filter filter; + + FilterRegistration(Filter filter) { + this.filter = filter; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.addFilter(filter); + } + } + + static final class MaxRerouteCountRegistration implements Registration { + private final int maxReRouteCount; + + MaxRerouteCountRegistration(int maxReRouteCount) { + this.maxReRouteCount = maxReRouteCount; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.maxReRouteCount(maxReRouteCount); + } + } + + static final class SecurityRegistration implements Registration { + private final HttpSecurity security; + + SecurityRegistration(HttpSecurity security) { + this.security = security; + } + + @Override + public void register(HttpRouting.Builder routing) { + routing.security(security); + } + } +}