This is an automated email from the ASF dual-hosted git repository.
cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git
The following commit(s) were added to refs/heads/master by this push:
new 6d95c936df Add jetty websocket support to Jetty12 (#298)
6d95c936df is described below
commit 6d95c936dfc7767f1cea23d1db98a8c7d7810378
Author: Paul <[email protected]>
AuthorDate: Thu Apr 18 14:27:47 2024 +0200
Add jetty websocket support to Jetty12 (#298)
* Update to latest Jetty 12.0.6 and add Jetty WebSocket bundles
This change makes it possible to use WebSockets via Felix HTTP Jetty 12
bundle.
There are still some strange things that need to be done to get it all to
work, but that's for a separate change request/bug report.
* Update pom.xml
* Remove websocket client dependency
* Remove tag
* Update to jetty 12.0.7
* Update pom.xml
Update to 12.0.8
* Add example of how to register a servlet that can upgrade to a Jetty
WebSocket
To try it out, deploy the whiteboard sample bundle to your OSGi environment
and execute the following snippet in your Chrome console.
```
const websocket = new
WebSocket("ws://localhost:8080/filtersample/websocket/example");
websocket.send("test");
```
* Send message for example as well
* Remove upgrade check, already done in super
* Extract abstract base class, which could be part of the Felix HTTP
Jetty12 bundle.
This abstracts away all the ugly code currently required to make websockets
work.
---
http/jetty12/pom.xml | 18 +++
http/samples/whiteboard/pom.xml | 34 +++++
.../felix/http/samples/whiteboard/Activator.java | 17 +++
.../whiteboard/FelixJettyWebSocketServlet.java | 145 +++++++++++++++++++++
.../samples/whiteboard/TestWebSocketServlet.java | 83 ++++++++++++
5 files changed, 297 insertions(+)
diff --git a/http/jetty12/pom.xml b/http/jetty12/pom.xml
index c67de9645b..0267fdb641 100644
--- a/http/jetty12/pom.xml
+++ b/http/jetty12/pom.xml
@@ -180,6 +180,9 @@
org.apache.commons.*
</Conditional-Package>
<Import-Package>
+ jakarta.annotation.*;resolution:=optional,
+ jakarta.transaction.*;resolution:=optional,
+ org.objectweb.asm.*;resolution:=optional,
sun.misc;resolution:=optional,
sun.nio.ch;resolution:=optional,
javax.imageio;resolution:=optional,
@@ -363,6 +366,11 @@
<artifactId>jetty-ee10-servlet</artifactId>
<version>${jetty.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee10.websocket</groupId>
+ <artifactId>jetty-ee10-websocket-jetty-server</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
@@ -413,6 +421,16 @@
<artifactId>jetty-session</artifactId>
<version>${jetty.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>jetty-websocket-jetty-api</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>jetty-websocket-jetty-server</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.servlet</artifactId>
diff --git a/http/samples/whiteboard/pom.xml b/http/samples/whiteboard/pom.xml
index ef0862fca3..4f40bff0ce 100644
--- a/http/samples/whiteboard/pom.xml
+++ b/http/samples/whiteboard/pom.xml
@@ -32,6 +32,10 @@
<version>3.0.0-SNAPSHOT</version>
<packaging>bundle</packaging>
+ <properties>
+ <jetty.version>12.0.8</jetty.version>
+ </properties>
+
<scm>
<connection>scm:git:https://github.com/apache/felix-dev.git</connection>
<developerConnection>scm:git:https://github.com/apache/felix-dev.git</developerConnection>
@@ -89,5 +93,35 @@
<version>2.0.0</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.http.jetty12</artifactId>
+ <version>1.0.3-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.http.api</artifactId>
+ <version>3.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee10.websocket</groupId>
+ <artifactId>jetty-ee10-websocket-jetty-server</artifactId>
+ <version>${jetty.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>jetty-websocket-jetty-api</artifactId>
+ <version>${jetty.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>jetty-websocket-jetty-server</artifactId>
+ <version>${jetty.version}</version>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java
b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java
index 5b92209475..8e4346641c 100644
---
a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java
+++
b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java
@@ -75,6 +75,23 @@ public final class Activator
filter2Props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT,
"(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME +
"=filtersample)");
context.registerService(Filter.class, filter2, filter2Props);
+
+ /**
+ * Register a WebSocket servlet on /filtersample/websocket/*.
+ * In the Chrome Console, this snippet can be used to send a message
to the WebSocket:
+ *
+ * const websocket = new
WebSocket("ws://localhost:8080/filtersample/websocket/example");
+ * websocket.send("test");
+ *
+ * This will log "test" to the stdout.
+ */
+ final TestWebSocketServlet webSocketServlet = new
TestWebSocketServlet("websocketservlet1");
+ final Dictionary<String, Object> webSocketServletProps = new
Hashtable<>();
+
webSocketServletProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN,
"/websocket/*");
+
webSocketServletProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT,
+ "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME +
"=filtersample)");
+ context.registerService(Servlet.class, webSocketServlet,
webSocketServletProps);
+
}
@Override
diff --git
a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java
b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java
new file mode 100644
index 0000000000..a8ed768291
--- /dev/null
+++
b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.http.samples.whiteboard;
+
+import java.io.IOException;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer;
+import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet;
+import
org.eclipse.jetty.ee10.websocket.server.internal.JettyServerFrameHandlerFactory;
+import org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter;
+import org.eclipse.jetty.websocket.core.server.WebSocketMappings;
+import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
+
+/**
+ * Abstract class that hides all Jetty Websocket specifics and provides a way
for the developer to focus on the actual WebSocket implementation.
+ * @author paulrutters
+ */
+public abstract class FelixJettyWebSocketServlet extends JettyWebSocketServlet
{
+
+ private final AtomicBoolean myFirstInitCall = new AtomicBoolean(true);
+ private final CountDownLatch myInitBarrier = new CountDownLatch(1);
+ private ServletContext myProxiedContext;
+ private ServletContextHandler myServletContextHandler;
+
+ @Override
+ public void init() throws ServletException {
+ // Init, delaying init call until service method is called...
+ }
+
+ @Override
+ public void destroy() {
+ // only call destroy when the servlet has been initialized
+ if (!myFirstInitCall.get()) {
+ // This is required because WebSocketServlet needs to have it's
destroy() method called as well
+ // Causes NPE otherwise when calling an WS endpoint
+ super.destroy();
+ }
+ }
+
+
+ // This is a workaround required for WebSockets to work in Jetty12, see
+ // https://www.eclipse.org/forums/index.php/t/1110140/
+ @Override
+ public synchronized ServletContext getServletContext() {
+ if (myProxiedContext == null) {
+ myProxiedContext = (ServletContext)
Proxy.newProxyInstance(JettyWebSocketServlet.class.getClassLoader(),
+ new Class[]{ServletContext.class}, (proxy, method,
methodArgs) -> {
+ final ServletContext osgiServletContext =
super.getServletContext();
+ if (!"getAttribute".equals(method.getName())) {
+ return method.invoke(osgiServletContext,
methodArgs);
+ }
+
+ final String name = (String) methodArgs[0];
+ Object value = osgiServletContext.getAttribute(name);
+ if (value == null && myProxiedContext != null) {
+ final ServletContext jettyServletContext =
myServletContextHandler.getServletContext();
+ value = jettyServletContext.getAttribute(name);
+ }
+ return value;
+ });
+ }
+
+ return myProxiedContext;
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
+ if (myFirstInitCall.compareAndSet(true, false)) {
+ try {
+ delayedInit();
+ } catch (Exception e) {
+ System.err.println("Error delayed init: " + e.getMessage());
+ } finally {
+ myInitBarrier.countDown();
+ }
+ } else {
+ try {
+ myInitBarrier.await();
+ } catch (final InterruptedException e) {
+ throw new ServletException("Timed out waiting for
initialisation", e);
+ }
+ }
+
+ // Call JettyWebSocketServlet service method to handle upgrade requests
+ super.service(req, resp);
+ }
+
+ private void delayedInit() throws ServletException {
+ // Make sure WebSockets are enabled in Jetty12
+ ensureWebSocketsInitialized();
+
+ // Overide the TCCL so that the internal factory can be found
+ // Jetty tries to use ServiceLoader, and their fallback is to
+ // use TCCL, it would be better if we could provide a loader...
+ final Thread currentThread = Thread.currentThread();
+ final ClassLoader tccl = currentThread.getContextClassLoader();
+
currentThread.setContextClassLoader(JettyWebSocketServlet.class.getClassLoader());
+ try {
+ super.init();
+ } finally {
+ currentThread.setContextClassLoader(tccl);
+ }
+ }
+
+ private void ensureWebSocketsInitialized() {
+ final ServletContext osgiServletContext = getServletContext();
+ myServletContextHandler =
ServletContextHandler.getServletContextHandler(osgiServletContext,
"WebSockets");
+
+ final JettyWebSocketServerContainer serverContainer =
JettyWebSocketServerContainer
+ .getContainer(osgiServletContext);
+ if (serverContainer == null) {
+ // Ensure WebSocket components are initialized in Jetty12
+ final ServletContext jettyServletContext =
myServletContextHandler.getServletContext();
+
WebSocketServerComponents.ensureWebSocketComponents(myServletContextHandler.getServer(),
+ myServletContextHandler);
+ WebSocketUpgradeFilter.ensureFilter(jettyServletContext);
+ WebSocketMappings.ensureMappings(myServletContextHandler);
+ JettyServerFrameHandlerFactory.getFactory(jettyServletContext);
+ JettyWebSocketServerContainer.ensureContainer(jettyServletContext);
+ }
+ }
+}
diff --git
a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java
b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java
new file mode 100644
index 0000000000..b09b7f6bbb
--- /dev/null
+++
b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.http.samples.whiteboard;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+
+import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory;
+import org.eclipse.jetty.websocket.api.Callback;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+public class TestWebSocketServlet extends FelixJettyWebSocketServlet {
+ private final String name;
+
+ public TestWebSocketServlet(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ doLog("Init with config [" + config + "]");
+ super.init(config);
+ }
+
+ private void doLog(String message) {
+ System.out.println("## [" + this.name + "] " + message);
+ }
+
+ @Override
+ protected void configure(JettyWebSocketServletFactory
jettyWebSocketServletFactory) {
+ doLog("Configuring WebSocket factory");
+ jettyWebSocketServletFactory.register(TestWebSocket.class);
+ }
+
+ @WebSocket
+ public static class TestWebSocket {
+ @OnWebSocketMessage
+ public void onText(final Session session, final String message) {
+ doLog("Received message: " + message);
+ }
+
+ @OnWebSocketOpen
+ public void onOpen(final Session session) {
+ doLog("Opened session: " + session);
+
+ // send a message to the client
+ session.sendText("Hello from server", Callback.NOOP);
+ }
+
+ @OnWebSocketError
+ public void onError(final Session session, final Throwable error) {
+ doLog("Error on session: " + session + " - " + error);
+ }
+
+ @OnWebSocketClose
+ public void onClose(final Session session, final int statusCode, final
String reason) {
+ doLog("Closed session: " + session + " - " + statusCode + " - " +
reason);
+ }
+
+ private void doLog(String message) {
+ System.out.println("## [" + this.getClass() + "] " + message);
+ }
+ }
+}