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