diff --git a/containers/helidon/pom.xml b/containers/helidon/pom.xml
new file mode 100644
index 0000000000..de3fb36aa0
--- /dev/null
+++ b/containers/helidon/pom.xml
@@ -0,0 +1,104 @@
+
+
+
+
+ 4.0.0
+
+
+ project
+ org.glassfish.jersey.containers
+ 3.1.99-SNAPSHOT
+
+
+ jersey-container-helidon
+ jar
+ jersey-container-helidon
+
+ Helidon Http Container
+
+
+ ${project.basedir}/target
+ ${project.basedir}/src/main/java11
+ ${project.basedir}/target17
+ ${project.basedir}/src/main/java17
+
+
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.activation
+ jakarta.activation-api
+
+
+ io.helidon.webserver
+ helidon-webserver
+ 4.0.9
+
+
+ io.helidon.tracing
+ helidon-tracing
+ 4.0.9
+
+
+ io.helidon.http
+ helidon-http
+ 4.0.9
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.hamcrest
+ hamcrest
+ 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..c99dd1370c
--- /dev/null
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainer.java
@@ -0,0 +1,112 @@
+/*
+ * 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 io.helidon.webserver.http.HttpRouting;
+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;
+
+public class HelidonHttpContainer implements Container, WebServer {
+
+ private final WebServer webServer;
+
+ private ApplicationHandler applicationHandler;
+
+ HelidonHttpContainer(Application application) {
+ this.applicationHandler = new ApplicationHandler(application, new WebServerBinder());
+ this.webServer = WebServer.builder().port(8080).routing(
+ HttpRouting.builder().register(
+ JerseySupport.create(this)
+ )).build();
+ }
+
+ @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) {
+ applicationHandler.onShutdown(this);
+
+ applicationHandler = new ApplicationHandler(configuration);
+ applicationHandler.onReload(this);
+ applicationHandler.onStartup(this);
+ if (webServer.isRunning()) {
+ webServer.stop();
+ webServer.start();
+ }
+ }
+
+ @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/HelidonHttpContainerFactory.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerFactory.java
new file mode 100644
index 0000000000..6f2e70790b
--- /dev/null
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerFactory.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 org.glassfish.jersey.server.ResourceConfig;
+
+import java.net.URI;
+
+public class HelidonHttpContainerFactory {
+
+ private HelidonHttpContainerFactory() {
+ }
+
+ public static WebServer createServer(URI baseUri, ResourceConfig config) {
+ return new HelidonHttpContainer(config);
+ }
+}
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..657b5896bc
--- /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));
+ }
+}
diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/JerseySupport.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/JerseySupport.java
new file mode 100644
index 0000000000..d9e05b7708
--- /dev/null
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/JerseySupport.java
@@ -0,0 +1,344 @@
+/*
+ * 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
+ *
+ */
+public class JerseySupport implements HttpService {
+
+ private static final System.Logger LOGGER = System.getLogger(JerseySupport.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 ApplicationHandler appHandler;
+ private final Container container;
+ private JerseySupport(Container container) {
+ this.appHandler = container.getApplicationHandler();
+ this.container = container;
+ }
+
+ static JerseySupport create(HelidonHttpContainer container) {
+ return new JerseySupport(container);
+ }
+
+ 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;
+ }
+ }
+
+ @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(container);
+ }
+ } 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/WebServerBinder.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/WebServerBinder.java
new file mode 100644
index 0000000000..33dde4c138
--- /dev/null
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/WebServerBinder.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;
+
+public class WebServerBinder 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/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/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..a35f374ff0
--- /dev/null
+++ b/containers/helidon/src/test/java/org/glassfish/jersey/helidon/AbstractHelidonServerTester.java
@@ -0,0 +1,119 @@
+/*
+ * 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 Jetty 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();
+ server = HelidonHttpContainerFactory.createServer(baseUri, config);
+ server.start();
+ LOGGER.log(Level.INFO, "Jetty-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, "Jetty-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/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..0088f45034 100644
--- a/containers/pom.xml
+++ b/containers/pom.xml
@@ -37,6 +37,7 @@
glassfish
grizzly2-http
grizzly2-servlet
+ helidon
jdk-http
jersey-servlet-core
jersey-servlet
]