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 <[email protected]>
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));
+ }
+ }
+
+}