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 a49d9f356b FELIX-6692 Add Jetty WebSocket support for jetty 11.x (#309)
a49d9f356b is described below

commit a49d9f356bac9a0ec87a32f5f9acff1716103ec4
Author: Eric Norman <enor...@apache.org>
AuthorDate: Wed May 1 00:02:55 2024 -0700

    FELIX-6692 Add Jetty WebSocket support for jetty 11.x (#309)
    
    * FELIX-6692 Add Jetty WebSocket support for jetty 11.x
    
    * FELIX-6692 rename jakarta websocket enable config for future expansion
    
    * FELIX-6692 add paxexam integration tests to verify the functionality
    
    * FELIX-6692 cleanup
    
    * FELIX-6692 merge changes from PR #310
    
    Rename org.apache.felix.jetty.websocket.enable to 
org.apache.felix.jetty.ee9.websocket.enable
    Incorporate the changes for using maybeStoreWebSocketContainerAttributes
    
    * FELIX-6692 renamed for consistency
---
 .../http/base/internal/HttpServiceController.java  |   9 +
 .../internal/whiteboard/WhiteboardManager.java     |  30 +++
 http/jetty/pom.xml                                 | 143 +++++++++++++-
 .../jetty/internal/ConfigMetaTypeProvider.java     |  12 ++
 .../felix/http/jetty/internal/JettyConfig.java     |  23 +++
 .../felix/http/jetty/internal/JettyService.java    |  83 +++++++++
 .../http/jetty/it/AbstractJettyTestSupport.java    | 185 ++++++++++++++++++
 .../jetty/it/JakartaEE9SpecificWebsocketIT.java    | 207 +++++++++++++++++++++
 .../http/jetty/it/JettyEE9SpecificWebsocketIT.java | 207 +++++++++++++++++++++
 .../jetty/it/MissingWebsocketDependenciesIT.java   |  89 +++++++++
 10 files changed, 983 insertions(+), 5 deletions(-)

diff --git 
a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
 
b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
index 570e9fe1ce..96439f5b1a 100644
--- 
a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
+++ 
b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
@@ -138,6 +138,15 @@ public final class HttpServiceController
         this.dispatcher.setWhiteboardManager(this.whiteboardManager);
     }
 
+    /**
+     * Stores an attribute in the to be created shared servlet context.
+     * @param key attribute key
+     * @param value attribute value
+     */
+    public void setAttributeSharedServletContext(String key, Object value) {
+        this.whiteboardManager.setAttributeSharedServletContext(key, value);
+    }
+
     /**
      * Stops the http and http whiteboard service.
      */
diff --git 
a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java
 
b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java
index f082122f7c..7d95499eb2 100644
--- 
a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java
+++ 
b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java
@@ -70,6 +70,7 @@ import 
org.apache.felix.http.base.internal.whiteboard.tracker.ResourceTracker;
 import 
org.apache.felix.http.base.internal.whiteboard.tracker.ServletContextHelperTracker;
 import org.apache.felix.http.base.internal.whiteboard.tracker.ServletTracker;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.Filter;
@@ -118,6 +119,7 @@ public final class WhiteboardManager
     private final FailureStateHandler failureStateHandler = new 
FailureStateHandler();
 
     private volatile ServletContext webContext;
+    private volatile Map<String, Object> attributesForSharedContext = new 
HashMap<>();
 
     /**
      * Create a new whiteboard http manager
@@ -203,6 +205,7 @@ public final class WhiteboardManager
         this.contextMap.clear();
         this.servicesMap.clear();
         this.failureStateHandler.clear();
+        this.attributesForSharedContext.clear();
         this.registry.reset();
     }
 
@@ -366,6 +369,8 @@ public final class WhiteboardManager
                         {
                             handlerList.add(handler);
                             Collections.sort(handlerList);
+                            setAttributes(handler.getSharedContext());
+
                             this.contextMap.put(info.getName(), handlerList);
 
                             // check for deactivate
@@ -402,6 +407,21 @@ public final class WhiteboardManager
         return false;
     }
 
+    /**
+     * Set the stored attributes on the shared servlet context.
+     * @param context the shared servlet context
+     */
+    private void setAttributes(@Nullable ServletContext context) {
+        if (context != null) {
+            attributesForSharedContext.forEach((key, value) -> {
+                if (key != null && value != null) {
+                    SystemLogger.LOGGER.info("Shared context found, setting 
stored attribute key: '{}', value: '{}'", key, value);
+                    context.setAttribute(key, value);
+                }
+            });
+        }
+    }
+
     /**
      * Remove a servlet context helper
      *
@@ -953,4 +973,14 @@ public final class WhiteboardManager
     {
         this.serviceRuntime.updateChangeCount();
     }
+
+    /**
+     * Stores an attribute in the to be created shared servlet context.
+     * @param key attribute key
+     * @param value attribute value
+     */
+    public void setAttributeSharedServletContext(String key, Object value) {
+        SystemLogger.LOGGER.info("Storing attribute for shared servlet 
context. Key '{}', value: '{}'", key, value);
+        this.attributesForSharedContext.put(key, value);
+    }
 }
diff --git a/http/jetty/pom.xml b/http/jetty/pom.xml
index bb16c026a0..9a9fd003b2 100644
--- a/http/jetty/pom.xml
+++ b/http/jetty/pom.xml
@@ -44,6 +44,9 @@
         <felix.java.version>11</felix.java.version>
         <jetty.version>11.0.20</jetty.version>
         <baseline.skip>true</baseline.skip>
+        <org.ops4j.pax.exam.version>4.13.3</org.ops4j.pax.exam.version>
+        <!-- To debug the pax process, override this with -D -->
+        <pax.vm.options>-Xmx512M</pax.vm.options>
     </properties>
 
     <build>
@@ -70,7 +73,11 @@
                                 // scan each of the artifacts to preserve the 
information found in any META-INF/services/* files
                                 project.artifacts.each() { artifact ->
 
-                                    if 
(artifact.getArtifactHandler().isAddedToClasspath() && 
!org.apache.maven.artifact.Artifact.SCOPE_TEST.equals( artifact.getScope() )) {
+                                    if 
(artifact.getArtifactHandler().isAddedToClasspath() && 
!org.apache.maven.artifact.Artifact.SCOPE_TEST.equals( artifact.getScope() )
+                                            && 
!"org.eclipse.jetty.websocket".equals(artifact.getGroupId()) // skip the 
optional websocket artifacts
+                                            && 
!"jetty-annotations".equals(artifact.getArtifactId()) // skip the transitive 
artifacts from the optional websocket artifacts
+                                            && 
!"jetty-plus".equals(artifact.getArtifactId())
+                                            && 
!"jetty-webapp".equals(artifact.getArtifactId())) { 
                                         def jar;
                                         try {
                                             jar = new 
java.util.jar.JarFile(artifact.file)
@@ -165,9 +172,15 @@
                             org.osgi.service.servlet.runtime,
                             org.osgi.service.servlet.runtime.dto,
                             org.osgi.service.servlet.whiteboard,
-                            !org.eclipse.jetty,
-                            !org.eclipse.jetty.version,
-                            org.eclipse.jetty.*,
+                            org.eclipse.jetty.alpn.server,
+                            org.eclipse.jetty.http.*,
+                            org.eclipse.jetty.http2.*,
+                            org.eclipse.jetty.io.*,
+                            org.eclipse.jetty.jmx.*,
+                            org.eclipse.jetty.security.*,
+                            org.eclipse.jetty.server.*,
+                            org.eclipse.jetty.servlet.*,
+                            org.eclipse.jetty.util.*,
                             org.apache.felix.http.jetty,
                             org.apache.felix.http.jakartawrappers,
                             org.apache.felix.http.javaxwrappers
@@ -320,6 +333,42 @@
                     </execution>
                 </executions>
             </plugin>
+
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                </configuration>
+            </plugin>
+            <!-- plugins for paxexam integration tests -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>integration-test</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>integration-test</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>verify</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                    <systemPropertyVariables>
+                        <jetty.version>${jetty.version}</jetty.version>
+                        
<bundle.filename>${basedir}/target/${project.build.finalName}.jar</bundle.filename>
+                        <pax.vm.options>${pax.vm.options}</pax.vm.options>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
@@ -410,7 +459,19 @@
             <artifactId>jetty-alpn-server</artifactId>
             <version>${jetty.version}</version>
         </dependency>
-       <dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.websocket</groupId>
+            <artifactId>websocket-jakarta-server</artifactId>
+            <version>${jetty.version}</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.websocket</groupId>
+            <artifactId>websocket-jetty-server</artifactId>
+            <version>${jetty.version}</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.service.servlet</artifactId>
             <version>2.0.0</version>
@@ -467,5 +528,77 @@
             <version>1.3.0</version>
             <scope>test</scope>
         </dependency>
+
+        <!-- an OSGi framework -->
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>7.0.5</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <version>1</version>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- Pax Exam -->
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-cm</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-forked</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-link-mvn</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-client</artifactId>
+            <version>${jetty.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.websocket</groupId>
+            <artifactId>websocket-jetty-client</artifactId>
+            <version>${jetty.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>4.2.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>2.0.13</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 </project>
diff --git 
a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java
 
b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java
index 9e50ed7673..89870aaa17 100644
--- 
a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java
+++ 
b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java
@@ -483,6 +483,18 @@ class ConfigMetaTypeProvider implements MetaTypeProvider
                 "The format of the request log entries. Only relevant if 
'Enable SLF4J Request Logging' is checked. Valid placeholders are described in 
https://www.eclipse.org/jetty/documentation/jetty-11/operations-guide/index.html#og-module-requestlog";,
                 CustomRequestLog.NCSA_FORMAT,
                 
bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTP_REQUEST_LOG_FORMAT)));
+
+        adList.add(new 
AttributeDefinitionImpl(JettyConfig.FELIX_JAKARTA_EE9_WEBSOCKET_ENABLE,
+                "Enable Jakarta EE9 standard WebSocket support",
+                "Whether to enable jakarta EE9 standard WebSocket support. 
Default is false.",
+                false,
+                
bundle.getBundleContext().getProperty(JettyConfig.FELIX_JAKARTA_EE9_WEBSOCKET_ENABLE)));
+        adList.add(new 
AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_EE9_WEBSOCKET_ENABLE,
+                "Enable Jetty specific EE9 WebSocket support",
+                "Whether to enable jetty specific WebSocket support. Default 
is false.",
+                false,
+                
bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_EE9_WEBSOCKET_ENABLE)));
+
         return new ObjectClassDefinition()
         {
 
diff --git 
a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
 
b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
index ef336ea61c..e8b07f72bd 100644
--- 
a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
+++ 
b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
@@ -268,6 +268,13 @@ public final class JettyConfig
     /** Felix specific property to specify the default protocol when 
negotiation fails  */
     public static final String FELIX_JETTY_ALPN_DEFAULT_PROTOCOL = 
"org.apache.felix.jetty.alpn.defaultProtocol";
 
+    /** Felix specific property to control whether to enable the standard 
jakarta.websocket EE9 APIs provided by Jakarta WebSocket 2.0 */
+    public static final String FELIX_JAKARTA_EE9_WEBSOCKET_ENABLE = 
"org.apache.felix.jakarta.ee9.websocket.enable";
+
+    /** Felix specific property to control whether to enable they 
Jetty-specific WebSocket APIs */
+    public static final String FELIX_JETTY_EE9_WEBSOCKET_ENABLE = 
"org.apache.felix.jetty.ee9.websocket.enable";
+
+
     private static String validateContextPath(String ctxPath)
     {
         // undefined, empty, or root context path
@@ -674,6 +681,22 @@ public final class JettyConfig
         return getLongProperty(FELIX_JETTY_STOP_TIMEOUT, -1l);
     }
 
+    /**
+     * Returns <code>true</code> if jakarta EE9 websocket is configured to be 
used (
+     * {@link #FELIX_JAKARTA_EE9_WEBSOCKET_ENABLE})
+     */
+    public boolean isUseJakartaEE9Websocket() {
+        return getBooleanProperty(FELIX_JAKARTA_EE9_WEBSOCKET_ENABLE, false);
+    }
+
+    /**
+     * Returns <code>true</code> if jetty websocket is configured to be used (
+     * {@link #FELIX_JETTY_WEBSOCKET_ENABLE})
+     */
+    public boolean isUseJettyEE9Websocket() {
+        return getBooleanProperty(FELIX_JETTY_EE9_WEBSOCKET_ENABLE, false);
+    }
+
     public void reset()
     {
         update(null);
diff --git 
a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
 
b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
index 4b7c32b784..d9e4c3768a 100644
--- 
a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
+++ 
b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
@@ -53,6 +53,7 @@ import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.util.thread.ThreadPool;
+import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
@@ -308,8 +309,18 @@ public final class JettyService
                 this.server.setStopTimeout(this.config.getStopTimeout());
             }
 
+            if (this.config.isUseJettyEE9Websocket()) {
+                maybeInitializeJettyEE9Websocket(context);
+            }
+
+            if (this.config.isUseJakartaEE9Websocket()) {
+                maybeInitializeJakartaEE9Websocket(context);
+            }
+
             this.server.start();
 
+            maybeStoreWebSocketContainerAttributes(context);
+
             // session id manager is only available after server is started
             
context.getSessionHandler().getSessionIdManager().getSessionHouseKeeper().setIntervalSec(
                     
this.config.getLongProperty(JettyConfig.FELIX_JETTY_SESSION_SCAVENGING_INTERVAL,
@@ -477,6 +488,78 @@ public final class JettyService
         return startConnector(connector);
     }
 
+    /**
+     * Initialize the jakarta EE9 websocket support for the servlet context 
handler.
+     * If the optional initializer class is not present then a warning will be 
logged.
+     *
+     * @param handler the sevlet context handler to initialize
+     */
+    private void maybeInitializeJakartaEE9Websocket(ServletContextHandler 
handler) {
+        if 
(isClassNameVisible("org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer"))
 {
+            // Ensure that JavaxWebSocketServletContainerInitializer is 
initialized,
+            // to setup the ServerContainer for this web application context.
+            
org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer.configure(handler,
 null);
+        } else {
+            SystemLogger.LOGGER.warn("Failed to initialize jakarta EE9 
standard websocket support since the initializer class was not found. "
+                    + "Check if the websocket-jakarta-server bundle is 
deployed.");
+        }
+    }
+
+    /**
+     * Initialize the jetty EE9 websocket support for the servlet context 
handler.
+     * If the optional initializer class is not present then a warning will be 
logged.
+     *
+     * @param handler the sevlet context handler to initialize
+     */
+    private void maybeInitializeJettyEE9Websocket(ServletContextHandler 
handler) {
+        if 
(isClassNameVisible("org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer"))
 {
+            // Ensure that JettyWebSocketServletContainerInitializer is 
initialized,
+            // to setup the JettyWebSocketServerContainer for this web 
application context.
+            
org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer.configure(handler,
 null);
+        } else {
+            SystemLogger.LOGGER.warn("Failed to initialize jetty specific 
websocket support since the initializer class was not found. "
+                    + "Check if the websocket-jetty-server bundle is 
deployed.");
+        }
+    }
+
+    /**
+     * Based on the configuration, store the WebSocket container attributes 
for the shared servlet context.
+     *
+     * @param context the context
+     */
+    private void maybeStoreWebSocketContainerAttributes(ServletContextHandler 
context) {
+        // when the server is started, retrieve the container attribute and
+        // set it on the shared servlet context once available
+        if (this.config.isUseJettyEE9Websocket() &&
+                
isClassNameVisible("org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer"))
 {
+            String attribute = 
JettyWebSocketServerContainer.JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE;
+            this.controller.setAttributeSharedServletContext(attribute, 
context.getServletContext().getAttribute(attribute));
+        }
+        if (this.config.isUseJakartaEE9Websocket() &&
+                
isClassNameVisible("org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer"))
 {
+            String attribute = 
org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer.ATTR_JAKARTA_SERVER_CONTAINER;
+            this.controller.setAttributeSharedServletContext(attribute, 
context.getServletContext().getAttribute(attribute));
+        }
+    }
+
+    /**
+     * Checks if an optional class name is visible to the bundle classloader
+     *
+     * @param className the class name to check
+     * @return true if the class is visible, false otherwise
+     */
+    private boolean isClassNameVisible(String className) {
+        boolean visible;
+        try {
+            // check if the class is visible to our classloader
+            getClass().getClassLoader().loadClass(className);
+            visible = true;
+        } catch (ClassNotFoundException e) {
+            visible = false;
+        }
+        return visible;
+    }
+
     private void configureSslContextFactory(final SslContextFactory.Server 
connector)
     {
         if (this.config.getKeystoreType() != null)
diff --git 
a/http/jetty/src/test/java/org/apache/felix/http/jetty/it/AbstractJettyTestSupport.java
 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/AbstractJettyTestSupport.java
new file mode 100644
index 0000000000..617f712b39
--- /dev/null
+++ 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/AbstractJettyTestSupport.java
@@ -0,0 +1,185 @@
+/*
+ * 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.jetty.it;
+
+import static org.ops4j.pax.exam.CoreOptions.bundle;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.keepCaches;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.CoreOptions.when;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.UUID;
+
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.ops4j.pax.exam.options.OptionalCompositeOption;
+import org.ops4j.pax.exam.options.SystemPropertyOption;
+import org.ops4j.pax.exam.options.UrlProvisionOption;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.ops4j.pax.exam.util.PathUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractJettyTestSupport {
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final String workingDirectory = 
String.format("%s/target/paxexam/%s/%s", PathUtils.getBaseDir(), 
getClass().getSimpleName(), UUID.randomUUID());
+
+    /**
+     * Provides a random path for a working directory below Maven's build 
target directory.
+     *
+     * @return the absolute path for working directory
+     */
+    protected String workingDirectory() {
+        return workingDirectory;
+    }
+
+    @Configuration
+    public Option[] configuration() throws IOException {
+        final String vmOpt = System.getProperty("pax.vm.options");
+        VMOption vmOption = null;
+        if (vmOpt != null && !vmOpt.isEmpty()) {
+            vmOption = new VMOption(vmOpt);
+        }
+
+        final int httpPort = findFreePort();
+
+        return options(
+            composite(
+                when(vmOption != null).useOptions(vmOption),
+                failOnUnresolvedBundles(),
+                keepCaches(),
+                localMavenRepo(),
+                CoreOptions.workingDirectory(workingDirectory()),
+                optionalRemoteDebug(),
+                
mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.http.servlet-api").version("3.0.0"),
+                testBundle("bundle.filename"),
+                junitBundles(),
+                awaitility(),
+
+                config(),
+                felixHttpConfig(httpPort)
+            ).add(
+                additionalOptions()
+            )
+        );
+    }
+
+    public static ModifiableCompositeOption awaitility() {
+        return composite(
+            
mavenBundle().groupId("org.awaitility").artifactId("awaitility").version("4.2.1"),
+            
mavenBundle().groupId("org.hamcrest").artifactId("hamcrest").version("2.2")
+        );
+    }
+
+    public static ModifiableCompositeOption config() {
+        return composite(
+            
mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.configadmin").version("1.9.26")
+        );
+    }
+
+    protected Option felixHttpConfig(final int httpPort) {
+        return newConfiguration("org.apache.felix.http")
+            .put("org.osgi.service.http.port", httpPort)
+            .asOption();
+    }
+
+    protected Option[] additionalOptions() throws IOException { // NOSONAR
+        return new Option[]{};
+    }
+
+    /**
+     * Finds a free local port.
+     *
+     * @return the free local port
+     */
+    public static int findFreePort() {
+        try (ServerSocket serverSocket = new ServerSocket(0)) {
+            return serverSocket.getLocalPort();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Provides an option to set the System property {@code 
pax.exam.osgi.unresolved.fail} to {@code "true"}.
+     *
+     * @return the property option
+     */
+    public static SystemPropertyOption failOnUnresolvedBundles() {
+        return systemProperty("pax.exam.osgi.unresolved.fail").value("true");
+    }
+
+    /**
+     * Reads the System property {@code maven.repo.local} and provides an 
option to set the System property {@code org.ops4j.pax.url.mvn.localRepository} 
when former is not empty.
+     *
+     * @return the property option
+     */
+    public static OptionalCompositeOption localMavenRepo() {
+        final String localRepository = System.getProperty("maven.repo.local", 
""); // PAXEXAM-543
+        return when(!localRepository.isBlank()).useOptions(
+            
systemProperty("org.ops4j.pax.url.mvn.localRepository").value(localRepository)
+        );
+    }
+
+    /**
+     * Reads the pathname of the test bundle from the given System property 
and provides a provisioning option.
+     *
+     * @param systemProperty the System property which contains the pathname 
of the test bundle
+     * @return the provisioning option
+     */
+    public static UrlProvisionOption testBundle(final String systemProperty) {
+        final String pathname = System.getProperty(systemProperty);
+        final File file = new File(pathname);
+        return bundle(file.toURI().toString());
+    }
+
+    /**
+     * Optionally configure remote debugging on the port supplied by the 
"debugPort"
+     * system property.
+     */
+    protected ModifiableCompositeOption optionalRemoteDebug() {
+        VMOption option = null;
+        String property = System.getProperty("debugPort");
+        if (property != null) {
+            option = 
vmOption(String.format("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%s",
 property));
+        }
+        return composite(option);
+    }
+
+    public static ModifiableCompositeOption spifly() {
+        return composite(
+            
mavenBundle().groupId("org.apache.aries.spifly").artifactId("org.apache.aries.spifly.dynamic.bundle").version("1.3.7"),
+            
mavenBundle().groupId("org.ow2.asm").artifactId("asm-analysis").version("9.7"),
+            
mavenBundle().groupId("org.ow2.asm").artifactId("asm-commons").version("9.7"),
+            
mavenBundle().groupId("org.ow2.asm").artifactId("asm-tree").version("9.7"),
+            
mavenBundle().groupId("org.ow2.asm").artifactId("asm-util").version("9.7"),
+            
mavenBundle().groupId("org.ow2.asm").artifactId("asm").version("9.7")
+        );
+    }
+}
diff --git 
a/http/jetty/src/test/java/org/apache/felix/http/jetty/it/JakartaEE9SpecificWebsocketIT.java
 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/JakartaEE9SpecificWebsocketIT.java
new file mode 100644
index 0000000000..70910ee970
--- /dev/null
+++ 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/JakartaEE9SpecificWebsocketIT.java
@@ -0,0 +1,207 @@
+/*
+ * 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.jetty.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.awaitility.Awaitility;
+import 
org.eclipse.jetty.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.servlet.whiteboard.HttpWhiteboardConstants;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.websocket.ClientEndpoint;
+import jakarta.websocket.DeploymentException;
+import jakarta.websocket.OnMessage;
+import jakarta.websocket.OnOpen;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+import jakarta.websocket.server.ServerContainer;
+import jakarta.websocket.server.ServerEndpoint;
+
+/**
+ *
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class JakartaEE9SpecificWebsocketIT extends AbstractJettyTestSupport {
+
+    @Inject
+    protected BundleContext bundleContext;
+
+    @Override
+    protected Option[] additionalOptions() throws IOException {
+        String jettyVersion = System.getProperty("jetty.version", "11.0.20");
+        return new Option[] {
+                spifly(),
+
+                // bundles for the server side
+                
mavenBundle().groupId("jakarta.websocket").artifactId("jakarta.websocket-api").version("2.0.0"),
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-alpn-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-webapp").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core-common").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core-server").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jakarta-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jakarta-common").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jakarta-server").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-servlet").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-xml").version(jettyVersion)
+        };
+    }
+
+    @Override
+    protected Option felixHttpConfig(int httpPort) {
+        return newConfiguration("org.apache.felix.http")
+                .put("org.osgi.service.http.port", httpPort)
+                .put("org.apache.felix.jakarta.ee9.websocket.enable", true)
+                .asOption();
+    }
+
+    @Test
+    public void testWebSocketConversation() throws Exception {
+        assertNotNull(bundleContext);
+        bundleContext.registerService(Servlet.class, new 
MyWebSocketInitServlet(), new Hashtable<>(Map.of(
+                HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, 
"/mywebsocket1"
+                )));
+
+        WebSocketContainer container = 
JakartaWebSocketClientContainerProvider.getContainer(null);
+
+        // Create client side endpoint
+        MyClientWebSocket clientEndpoint = new MyClientWebSocket();
+
+        // Attempt Connect
+        Object value = 
bundleContext.getServiceReference(HttpService.class).getProperty("org.osgi.service.http.port");
+        int httpPort = Integer.parseInt((String)value);
+        URI destUri = new URI(String.format("ws://localhost:%d/mywebsocket1", 
httpPort));
+        try (Session session = container.connectToServer(clientEndpoint, 
destUri)) {
+
+            // send a message from the client to the server
+            clientEndpoint.sendMessage("Hello WebSocket");
+
+            // wait for the async response from the server
+            Awaitility.await("waitForResponse")
+                .atMost(Duration.ofSeconds(30))
+                .pollDelay(Duration.ofMillis(200))
+                .until(() -> clientEndpoint.getLastMessage() != null);
+            assertEquals("Hello WebSocket", clientEndpoint.getLastMessage());
+        }
+    }
+
+    /**
+     * A servlet that declares the websocket during init
+     */
+    private static final class MyWebSocketInitServlet extends HttpServlet {
+        private static final long serialVersionUID = -6893620059263229183L;
+
+        @Override
+        public void init(ServletConfig config) throws ServletException {
+            super.init(config);
+
+            // Retrieve the ServerContainer from the ServletContext attributes.
+            ServletContext servletContext = config.getServletContext();
+            ServerContainer container = 
(ServerContainer)servletContext.getAttribute(ServerContainer.class.getName());
+
+            // Configure the ServerContainer.
+            container.setDefaultMaxTextMessageBufferSize(128 * 1024);
+
+            // Simple registration of your WebSocket endpoints.
+            try {
+                container.addEndpoint(MyServerWebSocket.class);
+            } catch (DeploymentException e) {
+                throw new ServletException(e);
+            }
+        }
+    }
+
+    /**
+     * WebSocket handler for the client side
+     */
+    @ClientEndpoint
+    public static class MyClientWebSocket {
+        private Session session;
+        private String lastMessage;
+
+        public String getLastMessage() {
+            return lastMessage;
+        }
+
+        @OnOpen
+        public void onConnect(Session session) {
+            this.session = session;
+        }
+
+        /**
+         * Send a message to the server side
+         * @param msg the message to send
+         */
+        public void sendMessage(String msg) throws IOException {
+            this.session.getBasicRemote().sendText(msg);
+        }
+
+        /**
+         * Receive a message from the server side
+         * @param msg the message
+         */
+        @OnMessage
+        public void onMessage(String msg) {
+            lastMessage = msg;
+        }
+    }
+
+    /**
+     * WebSocket handler for the server side
+     */
+    @ServerEndpoint(value = "/mywebsocket1")
+    public static class MyServerWebSocket {
+        /**
+         * Receive message sent from the client
+         * 
+         * @param session the session
+         * @param message the message
+         */
+        @OnMessage
+        public void onText(Session session, String message) throws IOException 
{
+            // echo a response back to the client 
+            session.getBasicRemote().sendText(message);
+        }
+    }
+
+}
diff --git 
a/http/jetty/src/test/java/org/apache/felix/http/jetty/it/JettyEE9SpecificWebsocketIT.java
 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/JettyEE9SpecificWebsocketIT.java
new file mode 100644
index 0000000000..162c4b7ecd
--- /dev/null
+++ 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/JettyEE9SpecificWebsocketIT.java
@@ -0,0 +1,207 @@
+/*
+ * 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.jetty.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.inject.Inject;
+
+import org.awaitility.Awaitility;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.servlet.whiteboard.HttpWhiteboardConstants;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+
+/**
+ *
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class JettyEE9SpecificWebsocketIT extends AbstractJettyTestSupport {
+
+    @Inject
+    protected BundleContext bundleContext;
+
+    @Override
+    protected Option[] additionalOptions() throws IOException {
+        String jettyVersion = System.getProperty("jetty.version", "11.0.20");
+        return new Option[] {
+                spifly(),
+
+                // bundles for the server side
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-webapp").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core-common").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core-server").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-api").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-common").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-server").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-servlet").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-xml").version(jettyVersion),
+
+                // additional bundles for the client side
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-alpn-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core-client").version(jettyVersion),
+                
mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-client").version(jettyVersion)
+        };
+    }
+
+    @Override
+    protected Option felixHttpConfig(int httpPort) {
+        return newConfiguration("org.apache.felix.http")
+                .put("org.osgi.service.http.port", httpPort)
+                .put("org.apache.felix.jetty.ee9.websocket.enable", true)
+                .asOption();
+    }
+
+
+    @Test
+    public void testWebSocketConversation() throws Exception {
+        assertNotNull(bundleContext);
+        bundleContext.registerService(Servlet.class, new 
MyWebSocketInitServlet(), new Hashtable<>(Map.of(
+                HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, 
"/mywebsocket1"
+                )));
+
+        HttpClientTransportOverHTTP transport = new 
HttpClientTransportOverHTTP();
+        HttpClient httpClient = new 
org.eclipse.jetty.client.HttpClient(transport);
+        WebSocketClient webSocketClient = new WebSocketClient(httpClient);
+        webSocketClient.start();
+
+        Object value = 
bundleContext.getServiceReference(HttpService.class).getProperty("org.osgi.service.http.port");
+        int httpPort = Integer.parseInt((String)value);
+        URI destUri = new URI(String.format("ws://localhost:%d/mywebsocket1", 
httpPort));
+
+        MyClientWebSocket clientWebSocket = new MyClientWebSocket();
+        ClientUpgradeRequest request = new ClientUpgradeRequest();
+        CompletableFuture<Session> future = 
webSocketClient.connect(clientWebSocket, destUri, request);
+        Session session = future.get();
+        assertNotNull(session);
+
+        // send a message from the client to the server
+        clientWebSocket.sendMessage("Hello WebSocket");
+
+        // wait for the async response from the server
+        Awaitility.await("waitForResponse")
+            .atMost(Duration.ofSeconds(30))
+            .pollDelay(Duration.ofMillis(200))
+            .until(() -> clientWebSocket.getLastMessage() != null);
+        assertEquals("Hello WebSocket", clientWebSocket.getLastMessage());
+    }
+
+    /**
+     * A servlet that declares the websocket during init
+     */
+    private static final class MyWebSocketInitServlet extends HttpServlet {
+        private static final long serialVersionUID = -6893620059263229183L;
+
+        @Override
+        public void init(ServletConfig config) throws ServletException {
+            super.init(config);
+
+            // Retrieve the JettyWebSocketServerContainer.
+            ServletContext servletContext = config.getServletContext();
+            JettyWebSocketServerContainer container = 
JettyWebSocketServerContainer.getContainer(servletContext);
+            assertNotNull(container);
+            container.addMapping("/mywebsocket1", (upgradeRequest, 
upgradeResponse) -> new MyServerWebSocket());
+        }
+    }
+
+    /**
+     * WebSocket handler for the client side
+     */
+    @WebSocket(maxTextMessageSize = 64 * 1024)
+    public static class MyClientWebSocket {
+        private Session session;
+        private String lastMessage;
+
+        public String getLastMessage() {
+            return lastMessage;
+        }
+
+        @OnWebSocketConnect
+        public void onConnect(Session session) {
+            this.session = session;
+        }
+
+        /**
+         * Send a message to the server side
+         * @param msg the message to send
+         */
+        public void sendMessage(String msg) {
+            this.session.getRemote().sendString(msg, WriteCallback.NOOP);
+        }
+
+        /**
+         * Receive a message from the server side
+         * @param msg the message
+         */
+        @OnWebSocketMessage
+        public void onMessage(String msg) {
+            lastMessage = msg;
+        }
+    }
+
+    /**
+     * WebSocket handler for the server side
+     */
+    @WebSocket(maxTextMessageSize = 64 * 1024)
+    public static class MyServerWebSocket {
+        /**
+         * Receive message sent from the client
+         * 
+         * @param session the session
+         * @param message the message
+         */
+        @OnWebSocketMessage
+        public void onText(Session session, String message) {
+            // echo a response back to the client 
+            session.getRemote().sendString(message, WriteCallback.NOOP);
+        }
+    }
+
+}
diff --git 
a/http/jetty/src/test/java/org/apache/felix/http/jetty/it/MissingWebsocketDependenciesIT.java
 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/MissingWebsocketDependenciesIT.java
new file mode 100644
index 0000000000..4527f85583
--- /dev/null
+++ 
b/http/jetty/src/test/java/org/apache/felix/http/jetty/it/MissingWebsocketDependenciesIT.java
@@ -0,0 +1,89 @@
+/*
+ * 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.jetty.it;
+
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+
+import org.awaitility.Awaitility;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.BundleContext;
+
+/**
+ *
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class MissingWebsocketDependenciesIT extends AbstractJettyTestSupport {
+
+    @Inject
+    protected BundleContext bundleContext;
+
+    @Override
+    protected Option felixHttpConfig(int httpPort) {
+        return newConfiguration("org.apache.felix.http")
+                .put("org.osgi.service.http.port", httpPort)
+                .put("org.apache.felix.jetty.ee9.websocket.enable", true)
+                .put("org.apache.felix.jakarta.ee9.websocket.enable", true)
+                .asOption();
+    }
+
+    @Test
+    public void testMissingDepencencyWarningLogs() throws Exception {
+        // should have warnings in the log file output
+        File logFile = new 
File("target/failsafe-reports/org.apache.felix.http.jetty.it.MissingWebsocketDependenciesIT-output.txt");
+        assertTrue(logFile.exists());
+
+        // wait for the log buffer to be written to the file
+        Awaitility.await("waitForLogs")
+            .atMost(Duration.ofSeconds(50))
+            .pollDelay(Duration.ofMillis(200))
+            .until(() -> containsString(logFile, 
"org.apache.felix.http.jetty[org.apache.felix.http]"));
+
+        assertTrue(containsString(logFile, 
"org.apache.felix.http.jetty[org.apache.felix.http] : Failed to initialize 
jetty specific websocket "
+                + "support since the initializer class was not found. Check if 
the websocket-jetty-server bundle is deployed."));
+        assertTrue(containsString(logFile, 
"org.apache.felix.http.jetty[org.apache.felix.http] : Failed to initialize 
jakarta EE9 standard websocket"
+                + " support since the initializer class was not found. Check 
if the websocket-jakarta-server bundle is deployed."));
+    }
+
+    /**
+     * Checks if the text is present in the file
+     * 
+     * @param file the file to check
+     * @param expected the text to look for
+     * @return true if the text was found, false otherwise
+     */
+    private boolean containsString(File file, String expected) throws 
IOException {
+        try (Stream<String> stream = Files.lines(file.toPath())) {
+            return stream.anyMatch(line -> line.contains(expected));
+        }
+    }
+
+}

Reply via email to