diff --git a/jetty-websocket/websocket-javax-client/pom.xml b/jetty-websocket/websocket-javax-client/pom.xml index 6c9f71034c7f..3212385271a5 100644 --- a/jetty-websocket/websocket-javax-client/pom.xml +++ b/jetty-websocket/websocket-javax-client/pom.xml @@ -34,6 +34,11 @@ jetty-client ${project.version} + + org.eclipse.jetty.toolchain + jetty-servlet-api + true + org.eclipse.jetty jetty-xml diff --git a/jetty-websocket/websocket-javax-client/src/main/java/module-info.java b/jetty-websocket/websocket-javax-client/src/main/java/module-info.java index c3bb0936c60c..41fd30729401 100644 --- a/jetty-websocket/websocket-javax-client/src/main/java/module-info.java +++ b/jetty-websocket/websocket-javax-client/src/main/java/module-info.java @@ -20,6 +20,8 @@ exports org.eclipse.jetty.websocket.javax.client; exports org.eclipse.jetty.websocket.javax.client.internal to org.eclipse.jetty.websocket.javax.server; + requires static jetty.servlet.api; + requires org.slf4j; requires org.eclipse.jetty.websocket.core.client; requires org.eclipse.jetty.websocket.javax.common; requires transitive org.eclipse.jetty.client; diff --git a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainerProvider.java b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainerProvider.java index 739b45317724..d5d6fb994947 100644 --- a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainerProvider.java +++ b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainerProvider.java @@ -18,7 +18,6 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.component.LifeCycle; -import org.eclipse.jetty.util.thread.ShutdownThread; import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; /** @@ -64,15 +63,8 @@ protected WebSocketContainer getContainer() public WebSocketContainer getContainer(HttpClient httpClient) { JavaxWebSocketClientContainer clientContainer = new JavaxWebSocketClientContainer(httpClient); - registerShutdown(clientContainer); + // See: https://github.com/eclipse-ee4j/websocket-api/issues/212 + LifeCycle.start(clientContainer); return clientContainer; } - - // See: https://github.com/eclipse-ee4j/websocket-api/issues/212 - private void registerShutdown(JavaxWebSocketClientContainer container) - { - // Register as JVM runtime shutdown hook. - ShutdownThread.register(container); - LifeCycle.start(container); - } } diff --git a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketShutdownContainer.java b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketShutdownContainer.java new file mode 100644 index 000000000000..6448e7245063 --- /dev/null +++ b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketShutdownContainer.java @@ -0,0 +1,73 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.client; + +import java.util.Set; +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; + +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

This manages the LifeCycle of {@link javax.websocket.WebSocketContainer} instances that are created with + * {@link javax.websocket.ContainerProvider}, if this code is being run from another ServletContainer, or if run inside a + * Jetty Server with the WebSocket client classes provided by the webapp.

+ * + *

This mechanism will not work if run with embedded Jetty or if the WebSocket client classes are provided by the server. + * In this case then the client {@link javax.websocket.WebSocketContainer} will register itself to be automatically shutdown + * with the Jetty {@code ContextHandler}.

+ */ +public class JavaxWebSocketShutdownContainer extends ContainerLifeCycle implements ServletContainerInitializer, ServletContextListener +{ + private static final Logger LOG = LoggerFactory.getLogger(JavaxWebSocketShutdownContainer.class); + + @Override + public void onStartup(Set> c, ServletContext ctx) throws ServletException + { + JavaxWebSocketClientContainer.setShutdownContainer(this); + ctx.addListener(this); + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + if (LOG.isDebugEnabled()) + LOG.debug("contextInitialized({}) {}", sce, this); + LifeCycle.start(this); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + if (LOG.isDebugEnabled()) + LOG.debug("contextDestroyed({}) {}", sce, this); + + LifeCycle.stop(this); + removeBeans(); + JavaxWebSocketClientContainer.setShutdownContainer(null); + } + + @Override + public String toString() + { + return String.format("%s@%x{%s, size=%s}", getClass().getSimpleName(), hashCode(), getState(), getBeans().size()); + } +} diff --git a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java index 883db46e5f6b..1776aae14f8c 100644 --- a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java +++ b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java @@ -22,6 +22,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import javax.websocket.ClientEndpoint; import javax.websocket.ClientEndpointConfig; @@ -33,6 +34,9 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.ShutdownThread; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException; @@ -43,6 +47,8 @@ import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketExtensionConfig; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandler; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Container for Client use of the javax.websocket API. @@ -52,6 +58,16 @@ @ManagedObject("JSR356 Client Container") public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer implements javax.websocket.WebSocketContainer { + private static final Logger LOG = LoggerFactory.getLogger(JavaxWebSocketClientContainer.class); + private static final AtomicReference SHUTDOWN_CONTAINER = new AtomicReference<>(); + + public static void setShutdownContainer(ContainerLifeCycle container) + { + SHUTDOWN_CONTAINER.set(container); + if (LOG.isDebugEnabled()) + LOG.debug("initialized {} to {}", String.format("%s@%x", SHUTDOWN_CONTAINER.getClass().getSimpleName(), SHUTDOWN_CONTAINER.hashCode()), container); + } + protected WebSocketCoreClient coreClient; protected Function coreClientFactory; private final JavaxWebSocketClientFrameHandlerFactory frameHandlerFactory; @@ -261,4 +277,123 @@ private ClientEndpointConfig getAnnotatedConfig(Object endpoint) throws Deployme return new AnnotatedClientEndpointConfig(anno); } + + @Override + protected void doStart() throws Exception + { + doClientStart(); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + doClientStop(); + } + + protected void doClientStart() + { + if (LOG.isDebugEnabled()) + LOG.debug("doClientStart() {}", this); + + // If we are running in Jetty register shutdown with the ContextHandler. + if (addToContextHandler()) + { + if (LOG.isDebugEnabled()) + LOG.debug("Shutdown registered with ContextHandler"); + return; + } + + // If we are running inside a different ServletContainer we can register with the SHUTDOWN_CONTAINER static. + ContainerLifeCycle shutdownContainer = SHUTDOWN_CONTAINER.get(); + if (shutdownContainer != null) + { + shutdownContainer.addManaged(this); + if (LOG.isDebugEnabled()) + LOG.debug("Shutdown registered with ShutdownContainer {}", shutdownContainer); + return; + } + + ShutdownThread.register(this); + if (LOG.isDebugEnabled()) + LOG.debug("Shutdown registered with ShutdownThread"); + } + + protected void doClientStop() + { + if (LOG.isDebugEnabled()) + LOG.debug("doClientStop() {}", this); + + // Remove from context handler if running in Jetty server. + removeFromContextHandler(); + + // Remove from the Shutdown Container. + ContainerLifeCycle shutdownContainer = SHUTDOWN_CONTAINER.get(); + if (shutdownContainer != null && shutdownContainer.contains(this)) + { + // Un-manage first as we don't want to call stop again while in STOPPING state. + shutdownContainer.unmanage(this); + shutdownContainer.removeBean(this); + } + + // If not running in a server we need to de-register with the shutdown thread. + ShutdownThread.deregister(this); + } + + private boolean addToContextHandler() + { + try + { + Object context = getClass().getClassLoader() + .loadClass("org.eclipse.jetty.server.handler.ContextHandler") + .getMethod("getCurrentContext") + .invoke(null); + + Object contextHandler = context.getClass() + .getMethod("getContextHandler") + .invoke(context); + + contextHandler.getClass() + .getMethod("addManaged", LifeCycle.class) + .invoke(contextHandler, this); + + return true; + } + catch (Throwable throwable) + { + if (LOG.isDebugEnabled()) + LOG.debug("error from addToContextHandler() for {}", this, throwable); + return false; + } + } + + private void removeFromContextHandler() + { + try + { + Object context = getClass().getClassLoader() + .loadClass("org.eclipse.jetty.server.handler.ContextHandler") + .getMethod("getCurrentContext") + .invoke(null); + + Object contextHandler = context.getClass() + .getMethod("getContextHandler") + .invoke(context); + + // Un-manage first as we don't want to call stop again while in STOPPING state. + contextHandler.getClass() + .getMethod("unmanage", Object.class) + .invoke(contextHandler, this); + + contextHandler.getClass() + .getMethod("removeBean", Object.class) + .invoke(contextHandler, this); + } + catch (Throwable throwable) + { + if (LOG.isDebugEnabled()) + LOG.debug("error from removeFromContextHandler() for {}", this, throwable); + } + } } diff --git a/jetty-websocket/websocket-javax-client/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/jetty-websocket/websocket-javax-client/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer new file mode 100644 index 000000000000..59e5f4b40551 --- /dev/null +++ b/jetty-websocket/websocket-javax-client/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +org.eclipse.jetty.websocket.javax.client.JavaxWebSocketShutdownContainer \ No newline at end of file diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java index c2acba49a2e1..957b4fe7b3ed 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java @@ -28,7 +28,6 @@ import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.listener.ContainerInitializer; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.thread.ThreadClassLoaderScope; import org.eclipse.jetty.websocket.core.WebSocketComponents; @@ -52,6 +51,18 @@ public class JavaxWebSocketServletContainerInitializer implements ServletContain public static final String HTTPCLIENT_ATTRIBUTE = "org.eclipse.jetty.websocket.javax.HttpClient"; private static final Logger LOG = LoggerFactory.getLogger(JavaxWebSocketServletContainerInitializer.class); + private final Configurator configurator; + + public JavaxWebSocketServletContainerInitializer() + { + this(null); + } + + public JavaxWebSocketServletContainerInitializer(Configurator configurator) + { + this.configurator = configurator; + } + /** * Test a ServletContext for {@code init-param} or {@code attribute} at {@code keyName} for * true or false setting that determines if the specified feature is enabled (or not). @@ -96,28 +107,7 @@ public static void configure(ServletContextHandler context, Configurator configu { if (!context.isStopped()) throw new IllegalStateException("configure should be called before starting"); - - // In this embedded-jetty usage, allow ServletContext.addListener() to - // add other ServletContextListeners (such as the ContextDestroyListener) after - // the initialization phase is over. (important for this SCI to function) - context.getServletContext().setExtendedListenerTypes(true); - - context.addEventListener(ContainerInitializer.asContextListener(new JavaxWebSocketServletContainerInitializer()) - .afterStartup((servletContext) -> - { - JavaxWebSocketServerContainer serverContainer = JavaxWebSocketServerContainer.getContainer(servletContext); - if (configurator != null) - { - try - { - configurator.accept(servletContext, serverContainer); - } - catch (DeploymentException e) - { - throw new RuntimeException("Failed to deploy WebSocket Endpoint", e); - } - } - })); + context.addServletContainerInitializer(new JavaxWebSocketServletContainerInitializer(configurator)); } /** @@ -270,6 +260,19 @@ public void onStartup(Set> c, ServletContext context) throws ServletExc } } } + + // Call the configurator after startup. + if (configurator != null) + { + try + { + configurator.accept(context, container); + } + catch (DeploymentException e) + { + throw new RuntimeException("Failed to deploy WebSocket Endpoint", e); + } + } } @SuppressWarnings("unchecked") diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java index 0195875ae1f8..f7f67507e567 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java @@ -296,4 +296,16 @@ protected void doStart() throws Exception deferredEndpointConfigs.clear(); } } + + @Override + protected void doClientStart() + { + // Do nothing. + } + + @Override + protected void doClientStop() + { + // Do nothing. + } } diff --git a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java index 3a27ec716074..77813d644606 100644 --- a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java +++ b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java @@ -17,7 +17,9 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.Random; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -46,7 +48,16 @@ public class WSServer extends LocalServer implements LocalFuzzer.Provider { private static final Logger LOG = LoggerFactory.getLogger(WSServer.class); private final Path testDir; - private ContextHandlerCollection contexts = new ContextHandlerCollection(); + private final ContextHandlerCollection contexts = new ContextHandlerCollection(); + + public WSServer() + { + String baseDirName = Long.toString(Math.abs(new Random().nextLong())); + this.testDir = MavenTestingUtils.getTargetTestingPath(baseDirName); + if (Files.exists(testDir)) + throw new IllegalStateException("TestDir already exists."); + FS.ensureDirExists(testDir); + } public WSServer(Path testDir) { diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/JavaxClientShutdownWithServerEmbeddedTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/JavaxClientShutdownWithServerEmbeddedTest.java new file mode 100644 index 000000000000..6a6fcc788ea0 --- /dev/null +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/JavaxClientShutdownWithServerEmbeddedTest.java @@ -0,0 +1,111 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests; + +import java.net.URI; +import java.util.Collection; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.websocket.ContainerProvider; +import javax.websocket.WebSocketContainer; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketShutdownContainer; +import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class JavaxClientShutdownWithServerEmbeddedTest +{ + private Server server; + private ServletContextHandler contextHandler; + private URI serverUri; + private HttpClient httpClient; + private volatile WebSocketContainer container; + + public class ContextHandlerShutdownServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + container = ContainerProvider.getWebSocketContainer(); + } + } + + @BeforeEach + public void before() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/"); + contextHandler.addServlet(new ServletHolder(new ContextHandlerShutdownServlet()), "/"); + server.setHandler(contextHandler); + + // Because we are using embedded we must manually add the Javax WS Client Shutdown SCI. + JavaxWebSocketShutdownContainer javaxWebSocketClientShutdown = new JavaxWebSocketShutdownContainer(); + contextHandler.addServletContainerInitializer(javaxWebSocketClientShutdown); + + server.start(); + serverUri = WSURI.toWebsocket(server.getURI()); + + httpClient = new HttpClient(); + httpClient.start(); + } + + @AfterEach + public void after() throws Exception + { + httpClient.stop(); + server.stop(); + } + + @Test + public void testShutdownWithContextHandler() throws Exception + { + ContentResponse response = httpClient.GET(serverUri); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + assertNotNull(container); + assertThat(container, instanceOf(JavaxWebSocketClientContainer.class)); + JavaxWebSocketClientContainer clientContainer = (JavaxWebSocketClientContainer)container; + assertThat(clientContainer.isRunning(), is(true)); + + // The container should be a bean on the ContextHandler. + Collection containedBeans = contextHandler.getBeans(WebSocketContainer.class); + assertThat(containedBeans.size(), is(1)); + assertThat(containedBeans.toArray()[0], is(container)); + + // The client should be attached to the servers LifeCycle and should stop with it. + server.stop(); + assertThat(clientContainer.isRunning(), is(false)); + assertThat(server.getContainedBeans(WebSocketContainer.class), empty()); + } +} diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/JavaxClientShutdownWithServerWebAppTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/JavaxClientShutdownWithServerWebAppTest.java new file mode 100644 index 000000000000..1f1562652f00 --- /dev/null +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/JavaxClientShutdownWithServerWebAppTest.java @@ -0,0 +1,191 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.websocket.ContainerProvider; +import javax.websocket.WebSocketContainer; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.Configurations; +import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest; +import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainerProvider; +import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketShutdownContainer; +import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +public class JavaxClientShutdownWithServerWebAppTest +{ + private WSServer server; + private HttpClient httpClient; + + @FunctionalInterface + interface ThrowingRunnable + { + void run() throws Exception; + } + + public void start(ThrowingRunnable configuration) throws Exception + { + server = new WSServer(); + configuration.run(); + server.start(); + httpClient = new HttpClient(); + httpClient.start(); + } + + @AfterEach + public void after() throws Exception + { + httpClient.stop(); + server.stop(); + } + + @WebServlet("/") + public static class ContextHandlerShutdownServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + ContainerProvider.getWebSocketContainer(); + } + } + + public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exception + { + WSServer.WebApp app = server.createWebApp(contextName); + + // Exclude the Javax WebSocket configuration from the webapp. + Configuration[] configurations = Configurations.getKnown().stream() + .filter(configuration -> !(configuration instanceof JavaxWebSocketConfiguration)) + .toArray(Configuration[]::new); + app.getWebAppContext().setConfigurations(configurations); + + // Copy over the individual jars required for Javax WebSocket. + app.createWebInf(); + app.copyLib(JavaxWebSocketClientContainerProvider.class, "websocket-javax-client.jar"); + app.copyLib(JavaxWebSocketContainer.class, "websocket-javax-common.jar"); + app.copyLib(ContainerLifeCycle.class, "jetty-util.jar"); + app.copyLib(CoreClientUpgradeRequest.class, "websocket-core-client.jar"); + app.copyLib(WebSocketComponents.class, "websocket-core-common.jar"); + app.copyLib(Response.class, "jetty-client.jar"); + app.copyLib(ByteBufferPool.class, "jetty-io.jar"); + app.copyLib(BadMessageException.class, "jetty-http.jar"); + + return app; + } + + @Test + public void websocketProvidedByServer() throws Exception + { + start(() -> + { + WSServer.WebApp app1 = server.createWebApp("app1"); + app1.createWebInf(); + app1.copyClass(ContextHandlerShutdownServlet.class); + app1.deploy(); + + WSServer.WebApp app2 = server.createWebApp("app2"); + app2.createWebInf(); + app2.copyClass(ContextHandlerShutdownServlet.class); + app2.deploy(); + + WSServer.WebApp app3 = server.createWebApp("app3"); + app3.createWebInf(); + app3.copyClass(ContextHandlerShutdownServlet.class); + app3.deploy(); + }); + + // Before connecting to the server there is only the containers created for the server component of each WebApp. + assertThat(server.isRunning(), is(true)); + assertThat(server.getContainedBeans(WebSocketContainer.class).size(), is(3)); + + // After hitting each WebApp with a request we now have an additional 3 client containers. + ContentResponse response = httpClient.GET(server.getServerUri().resolve("/app1")); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + response = httpClient.GET(server.getServerUri().resolve("/app2")); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + response = httpClient.GET(server.getServerUri().resolve("/app3")); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(server.getContainedBeans(WebSocketContainer.class).size(), is(6)); + + // All the websocket containers are removed on stopping of the server. + server.stop(); + assertThat(server.isRunning(), is(false)); + assertThat(server.getContainedBeans(WebSocketContainer.class).size(), is(0)); + } + + @Test + public void websocketProvidedByWebApp() throws Exception + { + start(() -> + { + WSServer.WebApp app1 = createWebSocketWebapp("app1"); + app1.copyClass(ContextHandlerShutdownServlet.class); + app1.deploy(); + + WSServer.WebApp app2 = createWebSocketWebapp("app2"); + app2.copyClass(ContextHandlerShutdownServlet.class); + app2.deploy(); + + WSServer.WebApp app3 = createWebSocketWebapp("app3"); + app3.copyClass(ContextHandlerShutdownServlet.class); + app3.deploy(); + }); + + // Before connecting to the server there is only the containers created for the server component of each WebApp. + assertThat(server.isRunning(), is(true)); + assertThat(server.getContainedBeans(WebSocketContainer.class).size(), is(0)); + + // After hitting each WebApp with a request we now have an additional 3 client containers. + ContentResponse response = httpClient.GET(server.getServerUri().resolve("/app1")); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + response = httpClient.GET(server.getServerUri().resolve("/app2")); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + response = httpClient.GET(server.getServerUri().resolve("/app3")); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + // Collect the toString result of the ShutdownContainers from the dump. + List results = Arrays.stream(server.getServer().dump().split("\n")) + .filter(line -> line.contains("+> " + JavaxWebSocketShutdownContainer.class.getSimpleName())) + .collect(Collectors.toList()); + + // We only have 3 Shutdown Containers and they all contain only 1 item to be shutdown. + assertThat(results.size(), is(3)); + for (String result : results) + { + assertThat(result, containsString("size=1")); + } + } +} diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java index d48b1dbed1a3..d6d8c327de87 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java @@ -18,7 +18,6 @@ import javax.servlet.ServletContext; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.listener.ContainerInitializer; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.server.WebSocketMappings; import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; @@ -32,6 +31,17 @@ public class JettyWebSocketServletContainerInitializer implements ServletContainerInitializer { private static final Logger LOG = LoggerFactory.getLogger(JettyWebSocketServletContainerInitializer.class); + private final Configurator configurator; + + public JettyWebSocketServletContainerInitializer() + { + this(null); + } + + public JettyWebSocketServletContainerInitializer(Configurator configurator) + { + this.configurator = configurator; + } public interface Configurator { @@ -50,18 +60,7 @@ public static void configure(ServletContextHandler context, Configurator configu { if (!context.isStopped()) throw new IllegalStateException("configure should be called before starting"); - - context.addEventListener( - ContainerInitializer - .asContextListener(new JettyWebSocketServletContainerInitializer()) - .afterStartup((servletContext) -> - { - if (configurator != null) - { - JettyWebSocketServerContainer container = JettyWebSocketServerContainer.getContainer(servletContext); - configurator.accept(servletContext, container); - } - })); + context.addServletContainerInitializer(new JettyWebSocketServletContainerInitializer(configurator)); } /** @@ -101,5 +100,10 @@ public void onStartup(Set> c, ServletContext context) JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.initialize(contextHandler); if (LOG.isDebugEnabled()) LOG.debug("onStartup {}", container); + + if (configurator != null) + { + configurator.accept(context, container); + } } }