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 0444ac4f73 FELIX-6692 jetty 12.x websockets new approach (based on #309) (#310) 0444ac4f73 is described below commit 0444ac4f73a3cb876511b00a2df49e7f16efee0c Author: Paul <p...@blueconic.com> AuthorDate: Wed May 1 09:04:05 2024 +0200 FELIX-6692 jetty 12.x websockets new approach (based on #309) (#310) * Revert "Add jetty websocket support to Jetty12 (#298)" This reverts commit 6d95c936dfc7767f1cea23d1db98a8c7d7810378. * FELIX-6692 Add Jetty WebSocket support for jetty 12.x - Apply 11.x approach to jetty12 bundle - Add two new classifiers 'with-jetty-ee10-websockets' and 'with-jakarta-ee10-websockets' to have a fat jar containing the appropriate websocket classes * Working example base on previous code. Do note that the workaround in FelixJettyWebSocketServlet are still required; the initialization code in the Jetty12 bundle doesn't seem to work * * Enable cross context support to allow WebSockets to be registered in Jetty 12. See https://github.com/jetty/jetty.project/blob/jetty-12.0.x/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java#L510 and https://github.com/jetty/jetty.project/issues/9774 * Add example based on registering the websocket to the main servlet context instead of the per-bundle one - * Added paxweb unit tests based on the Jetty11 work from #309 * Fix version * Fix message * Test casing * Update README.md * Make client optional * Add Jetty12 bundle to pom * Store the WebSocket container reference and set it on the shared servlet context once available This removes the need to set setCrossContextDispatchSupported, as the WS container is available on the proper servlet context itself * Update example to no longer use the root context. Removed other WebSocket example, as this is no longer needed with the new approach. Updated documentation. * Add servlet based example again, as it shows another example of how to register a WebSocket endpoint that abides to the servlet context it's registered to. * Rename class * Comment * Comments * Remove classloader code as it now also works without * Rename test class to EE10 * Small changes to README.md --- http/README.md | 123 +++---- http/jetty12/pom.xml | 377 +++++++++++++++++++-- .../jetty/internal/ConfigMetaTypeProvider.java | 11 + .../felix/http/jetty/internal/JettyConfig.java | 22 ++ .../felix/http/jetty/internal/JettyService.java | 86 ++++- .../http/jetty/it/AbstractJettyTestSupport.java | 185 ++++++++++ .../jetty/it/JakartaEE10SpecificWebsocketIT.java | 211 ++++++++++++ .../jetty/it/JettyEE10SpecificWebsocketIT.java | 209 ++++++++++++ .../jetty/it/MissingWebsocketDependenciesIT.java | 91 +++++ http/pom.xml | 1 + http/samples/whiteboard/pom.xml | 20 +- .../felix/http/samples/whiteboard/Activator.java | 31 +- .../whiteboard/FelixJettyWebSocketServlet.java | 109 +----- .../samples/whiteboard/TestWebSocketServlet.java | 32 +- ...t.java => TestWebSocketServletAlternative.java} | 24 +- 15 files changed, 1331 insertions(+), 201 deletions(-) diff --git a/http/README.md b/http/README.md index 875fcce940..10f8fd4d87 100644 --- a/http/README.md +++ b/http/README.md @@ -6,8 +6,8 @@ This is an implementation of the [R8.1 Whiteboard Specification for Jakarta Serv * Standard OSGi Http Whiteboard implementation * Run either with Jetty (version 11 or 12) bundle or inside your own application server using the servlet bridge * [Felix HTTP Jetty 12](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty12) is the preferred bundle of choice as it supports JavaEE 8 and JakartaEE 8 with the `javax` namespace, JakartaEE 9/10/11/future versions with the `jakarta` namespace. - * [Jetty WebSocket support](https://github.com/apache/felix-dev/pull/298), see example code [here](https://github.com/apache/felix-dev/blob/master/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java). - * [Felix HTTP Jetty 11](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty) is the predecessor of the Jetty 12 bundle, which shipped with [Jetty 9.4.x](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty/4.2.26) in the 4.x range, [Jetty 11.x](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty/5.1.10) in the 5.x range. + * [Jetty WebSocket support](https://github.com/apache/felix-dev/pull/310), see example code [here](https://github.com/apache/felix-dev/blob/master/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java). + * [Felix HTTP Jetty 11](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty) is the predecessor of the Jetty 12 bundle, which shipped with [Jetty 9.4.x](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty/4.2.26) in the 4.x range, [Jetty 11.x](https://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.jetty/5.1.10) in the 5.x range. * Correctly versioned Servlet API. ## Installing @@ -27,12 +27,17 @@ Note that as of version **3.x**, the Servlet APIs are **no longer** packaged wit `org.apache.felix.http.servlet-api` (or any other compatible Serlvet API bundle) to your classpath and deployment! -### Light bundle -If you would like to use your own Jetty jar instead of the one packaged with the Felix Jetty bundles, you can use the `light` variant. -When building the Felix Jetty bundle with Maven (`mvn clean install`), the `light` bundle will be created in the `target` directory, postfixed with `-light.jar`. -This jar can be deployed to your Felix OSGi environment, along with a compatible Jetty jar. +### Using classifiers: `light`, `with-jetty-ee10-websockets` and `with-jakarta-ee10-websockets` bundle +If you would like to use your own Jetty jars instead of the one packaged with the Felix Jetty bundles, you can use the variants with the following classifiers: +* `light` - A light version of the bundle that does not include the Jetty classes. This is useful when you want to use your own Jetty jars. Available for both Jetty bundles. +* `with-jetty-ee10-websockets` - A bundle that includes the classes required for Jetty WebSocket support for Jakarta EE10. Jetty12 bundle only. +* `with-jakarta-ee10-websockets` - A bundle that includes the classes required for Jakarta WebSocket support for Jakarta EE10. Jetty12 bundle only. -Or just use maven to include the dependency with the `light` classifier. +When building the Felix Jetty bundle with Maven (`mvn clean install`), the additional bundles will be created in the `target` directory, postfixed with classifier. +This jar can be deployed to your Felix OSGi environment, along with a compatible Jetty jars. +See the unit tests for the required bundles and versions that need to be deployed. + +Or just use maven to include the dependency with the proper classifier. ``` <dependency> <groupId>org.apache.felix</groupId> @@ -381,57 +386,59 @@ The service can both be configured using OSGi environment properties and using C this service is `"org.apache.felix.http"`. If you use both methods, Configuration Admin takes precedence. The following properties can be used (some legacy property names still exist but are not documented here on purpose). As properties might change over time, the actual list of properties can be found [here for the Jetty 12 bundle](https://github.com/apache/felix-dev/blob/master/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java) and [here for the Jetty 11 bundle](https://github.com/apache/felix-dev/blob/master/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/J [...] -| Property | Description | -|--|--| -| `org.apache.felix.http.host` | Host name or IP Address of the interface to listen on. The default is `null` causing Jetty to listen on all interfaces. | -| `org.osgi.service.http.port` | The port used for servlets and resources available via HTTP. The default is `8080`. See [port settings below](#http-port-settings) for additional information. A negative port number has the same effect as setting `org.apache.felix.http.enable` to `false`. | -| `org.osgi.service.http.port.secure` | The port used for servlets and resources available via HTTPS. The default is `8443`. See [port settings below](#http-port-settings) for additional information. A negative port number has the same effect as setting `org.apache.felix.https.enable` to `false`. | -| `org.apache.felix.http.context_path` | The servlet Context Path to use for the Http Service. If this property is not configured it defaults to "/". This must be a valid path starting with a slash and not ending with a slash (unless it is the root context). | -| `org.apache.felix.http.timeout` | Connection timeout in milliseconds. The default is `60000` (60 seconds). | -| `org.apache.felix.http.session.timeout` | Allows for the specification of the Session life time as a number of minutes. This property serves the same purpose as the `session-timeout` element in a Web Application descriptor. The default is "0" (zero) for no timeout at all. | -| `org.apache.felix.http.enable` | Flag to enable the use of HTTP. The default is `true`. | -| `org.apache.felix.https.enable` | Flag to enable the user of HTTPS. The default is `false`. | -| `org.apache.felix.https.keystore` | The name of the file containing the keystore. | -| `org.apache.felix.https.keystore.password` | The password for the keystore. | -| `org.apache.felix.https.keystore.key.password` | The password for the key in the keystore. | -| `org.apache.felix.https.truststore` | The name of the file containing the truststore. | -| `org.apache.felix.https.truststore.type` | The type of truststore to use. The default is `JKS`. | -| `org.apache.felix.https.truststore.password` | The password for the truststore. | -| `org.apache.felix.https.jetty.ciphersuites.excluded` | Configures comma-separated list of SSL cipher suites to *exclude*. Default is `null`, meaning that no cipher suite is excluded. | -| `org.apache.felix.https.jetty.ciphersuites.included` | Configures comma-separated list of SSL cipher suites to *include*. Default is `null`, meaning that the default cipher suites are used. | -| `org.apache.felix.https.jetty.protocols.excluded` | Configures comma-separated list of SSL protocols (e.g. SSLv3, TLSv1.0, TLSv1.1, TLSv1.2) to *exclude*. Default is `null`, meaning that no protocol is excluded. | -| `org.apache.felix.https.jetty.protocols.included` | Configures comma-separated list of SSL protocols to *include*. Default is `null`, meaning that the default protocols are used. | -| `org.apache.felix.https.clientcertificate` | Flag to determine if the HTTPS protocol requires, wants or does not use client certificates. Legal values are `needs`, `wants` and `none`. The default is `none`. | -| `org.apache.felix.http.jetty.headerBufferSize` | Size of the buffer for request and response headers, in bytes. Default is 16 KB. | -| `org.apache.felix.http.jetty.requestBufferSize` | Size of the buffer for requests not fitting the header buffer, in bytes. Default is 8 KB. | -| `org.apache.felix.http.jetty.responseBufferSize` | Size of the buffer for responses, in bytes. Default is 24 KB. | -| `org.apache.felix.http.jetty.maxFormSize` | The maximum size accepted for a form post, in bytes. Defaults to 200 KB. | -| `org.apache.felix.http.mbeans` | If `true`, enables the MBean server functionality. The default is `false`. | -| `org.apache.felix.http.jetty.sendServerHeader` | If `false`, the `Server` HTTP header is no longer included in responses. The default is `false`. | -| `org.eclipse.jetty.servlet.SessionCookie` | Name of the cookie used to transport the Session ID. The default is `JSESSIONID`. | -| `org.eclipse.jetty.servlet.SessionURL` | Name of the request parameter to transport the Session ID. The default is `jsessionid`. | -| `org.eclipse.jetty.servlet.SessionDomain` | Domain to set on the session cookie. The default is `null`. | -| `org.eclipse.jetty.servlet.SessionPath` | The path to set on the session cookie. The default is the configured session context path ("/"). | -| `org.eclipse.jetty.servlet.MaxAge` | The maximum age value to set on the cookie. The default is "-1". | -| `org.eclipse.jetty.UriComplianceMode` | The URI compliance mode to set. The default is [DEFAULT](https://eclipse.dev/jetty/javadoc/jetty-12/org/eclipse/jetty/http/UriCompliance.html#DEFAULT). See [documentation](https://eclipse.dev/jetty/documentation/jetty-12/programming-guide/index.html#pg-server-compliance-uri.) and [possible modes](https://github.com/jetty/jetty.project/blob/jetty-12.0.x/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java#L186C107-L186C113). | -| `org.apache.felix.proxy.load.balancer.connection.enable` | Set this to `true` when running Felix HTTP behind a (offloading) proxy or load balancer which rewrites the requests. The default is `false`. | -| `org.apache.felix.http.runtime.init.` | Properties starting with this prefix are added as service registration properties to the HttpServiceRuntime service. The prefix is removed for the property name. | -| `org.apache.felix.jetty.gziphandler.enable` | Whether the server should use a server-wide gzip handler. Default is false. | -| `org.apache.felix.jetty.gzip.minGzipSize` | The minimum response size to trigger dynamic compression. Default is GzipHandler.DEFAULT_MIN_GZIP_SIZE. | -| `org.apache.felix.jetty.gzip.inflateBufferSize` | The size in bytes of the buffer to inflate compressed request, or <= 0 for no inflation. Default is -1. | -| `org.apache.felix.jetty.gzip.syncFlush` | True if Deflater#SYNC_FLUSH should be used, else Deflater#NO_FLUSH will be used. Default is false. | -| `org.apache.felix.jetty.gzip.includedMethods` | The additional http methods to include in compression. Default is none. | -| `org.apache.felix.jetty.gzip.excludedMethods` | The additional http methods to exclude in compression. Default is none. | -| `org.apache.felix.jetty.gzip.includedPaths` | The additional path specs to include. Inclusion takes precedence over exclusion. Default is none. | -| `org.apache.felix.jetty.gzip.excludedPaths` | The additional path specs to exclude. Inclusion takes precedence over exclusion. Default is none. | -| `org.apache.felix.jetty.gzip.includedMimeTypes` | The included mime types. Inclusion takes precedence over exclusion. Default is none. | -| `org.apache.felix.jetty.gzip.excludedMimeTypes` | The excluded mime types. Inclusion takes precedence over exclusion. Default is none. | -| `org.apache.felix.http2.enable` | Whether to enable HTTP/2. Default is false. | -| `org.apache.felix.jetty.http2.maxConcurrentStreams` | The max number of concurrent streams per connection. Default is 128. | -| `org.apache.felix.jetty.http2.initialStreamRecvWindow` | The initial stream receive window (client to server). Default is 524288. | -| `org.apache.felix.jetty.http2.initialSessionRecvWindow` | The initial session receive window (client to server). Default is 1048576. | -| `org.apache.felix.jetty.alpn.protocols` | The ALPN protocols to consider. Default is h2, http/1.1. | -| `org.apache.felix.jetty.alpn.defaultProtocol` | The default protocol when negotiation fails. Default is http/1.1. | +| Property | Description [...] +|--|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `org.apache.felix.http.host` | Host name or IP Address of the interface to listen on. The default is `null` causing Jetty to listen on all interfaces. [...] +| `org.osgi.service.http.port` | The port used for servlets and resources available via HTTP. The default is `8080`. See [port settings below](#http-port-settings) for additional information. A negative port number has the same effect as setting `org.apache.felix.http.enable` to `false`. [...] +| `org.osgi.service.http.port.secure` | The port used for servlets and resources available via HTTPS. The default is `8443`. See [port settings below](#http-port-settings) for additional information. A negative port number has the same effect as setting `org.apache.felix.https.enable` to `false`. [...] +| `org.apache.felix.http.context_path` | The servlet Context Path to use for the Http Service. If this property is not configured it defaults to "/". This must be a valid path starting with a slash and not ending with a slash (unless it is the root context). [...] +| `org.apache.felix.http.timeout` | Connection timeout in milliseconds. The default is `60000` (60 seconds). [...] +| `org.apache.felix.http.session.timeout` | Allows for the specification of the Session life time as a number of minutes. This property serves the same purpose as the `session-timeout` element in a Web Application descriptor. The default is "0" (zero) for no timeout at all. [...] +| `org.apache.felix.http.enable` | Flag to enable the use of HTTP. The default is `true`. [...] +| `org.apache.felix.https.enable` | Flag to enable the user of HTTPS. The default is `false`. [...] +| `org.apache.felix.https.keystore` | The name of the file containing the keystore. [...] +| `org.apache.felix.https.keystore.password` | The password for the keystore. [...] +| `org.apache.felix.https.keystore.key.password` | The password for the key in the keystore. [...] +| `org.apache.felix.https.truststore` | The name of the file containing the truststore. [...] +| `org.apache.felix.https.truststore.type` | The type of truststore to use. The default is `JKS`. [...] +| `org.apache.felix.https.truststore.password` | The password for the truststore. [...] +| `org.apache.felix.https.jetty.ciphersuites.excluded` | Configures comma-separated list of SSL cipher suites to *exclude*. Default is `null`, meaning that no cipher suite is excluded. [...] +| `org.apache.felix.https.jetty.ciphersuites.included` | Configures comma-separated list of SSL cipher suites to *include*. Default is `null`, meaning that the default cipher suites are used. [...] +| `org.apache.felix.https.jetty.protocols.excluded` | Configures comma-separated list of SSL protocols (e.g. SSLv3, TLSv1.0, TLSv1.1, TLSv1.2) to *exclude*. Default is `null`, meaning that no protocol is excluded. [...] +| `org.apache.felix.https.jetty.protocols.included` | Configures comma-separated list of SSL protocols to *include*. Default is `null`, meaning that the default protocols are used. [...] +| `org.apache.felix.https.clientcertificate` | Flag to determine if the HTTPS protocol requires, wants or does not use client certificates. Legal values are `needs`, `wants` and `none`. The default is `none`. [...] +| `org.apache.felix.http.jetty.headerBufferSize` | Size of the buffer for request and response headers, in bytes. Default is 16 KB. [...] +| `org.apache.felix.http.jetty.requestBufferSize` | Size of the buffer for requests not fitting the header buffer, in bytes. Default is 8 KB. [...] +| `org.apache.felix.http.jetty.responseBufferSize` | Size of the buffer for responses, in bytes. Default is 24 KB. [...] +| `org.apache.felix.http.jetty.maxFormSize` | The maximum size accepted for a form post, in bytes. Defaults to 200 KB. [...] +| `org.apache.felix.http.mbeans` | If `true`, enables the MBean server functionality. The default is `false`. [...] +| `org.apache.felix.http.jetty.sendServerHeader` | If `false`, the `Server` HTTP header is no longer included in responses. The default is `false`. [...] +| `org.eclipse.jetty.servlet.SessionCookie` | Name of the cookie used to transport the Session ID. The default is `JSESSIONID`. [...] +| `org.eclipse.jetty.servlet.SessionURL` | Name of the request parameter to transport the Session ID. The default is `jsessionid`. [...] +| `org.eclipse.jetty.servlet.SessionDomain` | Domain to set on the session cookie. The default is `null`. [...] +| `org.eclipse.jetty.servlet.SessionPath` | The path to set on the session cookie. The default is the configured session context path ("/"). [...] +| `org.eclipse.jetty.servlet.MaxAge` | The maximum age value to set on the cookie. The default is "-1". [...] +| `org.eclipse.jetty.UriComplianceMode` | The URI compliance mode to set. The default is [DEFAULT](https://eclipse.dev/jetty/javadoc/jetty-12/org/eclipse/jetty/http/UriCompliance.html#DEFAULT). See [documentation](https://eclipse.dev/jetty/documentation/jetty-12/programming-guide/index.html#pg-server-compliance-uri.) and [possible modes](https://github.com/jetty/jetty.project/blob/jetty-12.0.x/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.jav [...] +| `org.apache.felix.proxy.load.balancer.connection.enable` | Set this to `true` when running Felix HTTP behind a (offloading) proxy or load balancer which rewrites the requests. The default is `false`. [...] +| `org.apache.felix.http.runtime.init.` | Properties starting with this prefix are added as service registration properties to the HttpServiceRuntime service. The prefix is removed for the property name. [...] +| `org.apache.felix.jetty.gziphandler.enable` | Whether the server should use a server-wide gzip handler. Default is false. [...] +| `org.apache.felix.jetty.gzip.minGzipSize` | The minimum response size to trigger dynamic compression. Default is GzipHandler.DEFAULT_MIN_GZIP_SIZE. [...] +| `org.apache.felix.jetty.gzip.inflateBufferSize` | The size in bytes of the buffer to inflate compressed request, or <= 0 for no inflation. Default is -1. [...] +| `org.apache.felix.jetty.gzip.syncFlush` | True if Deflater#SYNC_FLUSH should be used, else Deflater#NO_FLUSH will be used. Default is false. [...] +| `org.apache.felix.jetty.gzip.includedMethods` | The additional http methods to include in compression. Default is none. [...] +| `org.apache.felix.jetty.gzip.excludedMethods` | The additional http methods to exclude in compression. Default is none. [...] +| `org.apache.felix.jetty.gzip.includedPaths` | The additional path specs to include. Inclusion takes precedence over exclusion. Default is none. [...] +| `org.apache.felix.jetty.gzip.excludedPaths` | The additional path specs to exclude. Inclusion takes precedence over exclusion. Default is none. [...] +| `org.apache.felix.jetty.gzip.includedMimeTypes` | The included mime types. Inclusion takes precedence over exclusion. Default is none. [...] +| `org.apache.felix.jetty.gzip.excludedMimeTypes` | The excluded mime types. Inclusion takes precedence over exclusion. Default is none. [...] +| `org.apache.felix.http2.enable` | Whether to enable HTTP/2. Default is false. [...] +| `org.apache.felix.jetty.http2.maxConcurrentStreams` | The max number of concurrent streams per connection. Default is 128. [...] +| `org.apache.felix.jetty.http2.initialStreamRecvWindow` | The initial stream receive window (client to server). Default is 524288. [...] +| `org.apache.felix.jetty.http2.initialSessionRecvWindow` | The initial session receive window (client to server). Default is 1048576. [...] +| `org.apache.felix.jetty.alpn.protocols` | The ALPN protocols to consider. Default is h2, http/1.1. [...] +| `org.apache.felix.jetty.alpn.defaultProtocol` | The default protocol when negotiation fails. Default is http/1.1. [...] +| `org.apache.felix.jakarta.ee10.websocket.enable` | Enables Jakarta EE10 websocket support. Default is false. Jetty12 only. [...] +| `org.apache.felix.jetty.ee10.websocket.enable` | Enables Jetty EE10 websocket support. Default is false. Jetty12 only. [...] ### Multiple Servers diff --git a/http/jetty12/pom.xml b/http/jetty12/pom.xml index 0267fdb641..450d6ed274 100644 --- a/http/jetty12/pom.xml +++ b/http/jetty12/pom.xml @@ -43,6 +43,9 @@ <felix.java.version>17</felix.java.version> <jetty.version>12.0.8</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> @@ -69,7 +72,12 @@ // 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 + && !"org.eclipse.jetty.ee10.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) @@ -164,9 +172,17 @@ 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.session.*, + org.eclipse.jetty.server.*, + org.eclipse.jetty.util.*, + !org.eclipse.jetty.ee10.websocket.*, + org.eclipse.jetty.ee10.servlet.*, org.apache.felix.http.jetty, org.apache.felix.http.jakartawrappers, org.apache.felix.http.javaxwrappers @@ -180,9 +196,6 @@ org.apache.commons.* </Conditional-Package> <Import-Package> - jakarta.annotation.*;resolution:=optional, - jakarta.transaction.*;resolution:=optional, - org.objectweb.asm.*;resolution:=optional, sun.misc;resolution:=optional, sun.nio.ch;resolution:=optional, javax.imageio;resolution:=optional, @@ -319,8 +332,257 @@ </instructions> </configuration> </execution> + <execution> + <id>with-jetty-ee10-websockets</id> + <goals> + <goal>bundle</goal> + </goals> + <configuration> + <classifier>with-jetty-ee10-websockets</classifier> + <instructions> + <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> + <Bundle-Version>${project.version}</Bundle-Version> + <X-Jetty-Version> + ${jetty.version} + </X-Jetty-Version> + <Bundle-Activator> + org.apache.felix.http.jetty.internal.JettyActivator + </Bundle-Activator> + <Export-Package> + org.osgi.service.http, + org.osgi.service.http.context, + org.osgi.service.http.runtime, + org.osgi.service.http.runtime.dto, + org.osgi.service.http.whiteboard, + org.osgi.service.servlet.context, + org.osgi.service.servlet.runtime, + org.osgi.service.servlet.runtime.dto, + org.osgi.service.servlet.whiteboard, + 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.session.*, + org.eclipse.jetty.server.*, + org.eclipse.jetty.util.*, + org.eclipse.jetty.ee10.servlet.*, + !org.eclipse.jetty.ee10.websocket.jakarta.*, + org.eclipse.jetty.ee10.websocket.*, + org.eclipse.jetty.websocket.*, + org.apache.felix.http.jetty, + org.apache.felix.http.jakartawrappers, + org.apache.felix.http.javaxwrappers + </Export-Package> + <Private-Package> + org.apache.felix.http.base.*, + org.apache.felix.http.jetty.*, + org.eclipse.jetty.version + </Private-Package> + <Conditional-Package> + org.apache.commons.* + </Conditional-Package> + <Import-Package> + org.eclipse.jetty.client;resolution:=optional, + sun.misc;resolution:=optional, + sun.nio.ch;resolution:=optional, + javax.imageio;resolution:=optional, + javax.sql;resolution:=optional, + org.ietf.jgss;resolution:=optional, + org.osgi.service.cm;resolution:=optional;version="[1.3,2)", + org.osgi.service.event;resolution:=optional;version="[1.2,2)", + org.osgi.service.log;resolution:=optional;version="[1.3,2)", + org.osgi.service.metatype;resolution:=optional;version="[1.1,2)", + org.osgi.service.useradmin;resolution:=optional;version="[1.1,2)", + org.osgi.service.http;version="[1.2.1,1.3)", + org.osgi.service.http.context;version="[1.1,1.2)", + org.osgi.service.http.runtime;version="[1.1,1.2)", + org.osgi.service.http.runtime.dto;version="[1.1,1.2)", + org.slf4j;version="[1.0,3.0)", + * + </Import-Package> + <DynamicImport-Package> + org.osgi.service.cm;version="[1.3,2)", + org.osgi.service.event;version="[1.2,2)", + org.osgi.service.log;version="[1.3,2)", + org.osgi.service.metatype;version="[1.4,2)" + </DynamicImport-Package> + <Provide-Capability> + osgi.implementation;osgi.implementation="osgi.http";version:Version="1.1"; + uses:="javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard", + osgi.implementation;osgi.implementation="osgi.http";version:Version="2.0"; + uses:="jakarta.servlet,jakarta.servlet.http,org.osgi.service.servlet.context,org.osgi.service.servlet.whiteboard", + osgi.service;objectClass:List<String>="org.osgi.service.servlet.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.servlet.runtime,org.osgi.service.servlet.runtime.dto", + osgi.service;objectClass:List<String>="org.osgi.service.http.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto", + osgi.service;objectClass:List<String>="org.osgi.service.http.HttpService"; + uses:="org.osgi.service.http", + osgi.serviceloader;osgi.serviceloader="org.eclipse.jetty.http.HttpFieldPreEncoder" + </Provide-Capability> + <Require-Capability> + osgi.contract;filter:="(&(osgi.contract=JavaServlet)(version=4.0))", + osgi.contract;filter:="(&(osgi.contract=JakartaServlet)(version=6.0))", + osgi.extender;filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional, + osgi.extender;filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, + osgi.serviceloader;filter:="(osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder)";resolution:=optional;cardinality:=multiple, + osgi.serviceloader;filter:="(osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server)";resolution:=optional;cardinality:=multiple + </Require-Capability> + <Include-Resource> + {maven-resources},${project.build.directory}/serviceloader-resources + </Include-Resource> + <_removeheaders> + Private-Package,Conditional-Package,Include-Resource + </_removeheaders> + </instructions> + </configuration> + </execution> + <execution> + <id>with-jakarta-ee10-websockets</id> + <goals> + <goal>bundle</goal> + </goals> + <configuration> + <classifier>with-jakarta-ee10-websockets</classifier> + <instructions> + <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> + <Bundle-Version>${project.version}</Bundle-Version> + <X-Jetty-Version> + ${jetty.version} + </X-Jetty-Version> + <Bundle-Activator> + org.apache.felix.http.jetty.internal.JettyActivator + </Bundle-Activator> + <Export-Package> + org.osgi.service.http, + org.osgi.service.http.context, + org.osgi.service.http.runtime, + org.osgi.service.http.runtime.dto, + org.osgi.service.http.whiteboard, + org.osgi.service.servlet.context, + org.osgi.service.servlet.runtime, + org.osgi.service.servlet.runtime.dto, + org.osgi.service.servlet.whiteboard, + 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.session.*, + org.eclipse.jetty.server.*, + org.eclipse.jetty.util.*, + !org.eclipse.jetty.ee10.websocket.server.*, + !org.eclipse.jetty.ee10.websocket.servlet.*, + org.eclipse.jetty.ee10.websocket.jakarta.*, + org.eclipse.jetty.ee10.servlet.*, + org.eclipse.jetty.websocket.*, + org.apache.felix.http.jetty, + org.apache.felix.http.jakartawrappers, + org.apache.felix.http.javaxwrappers + </Export-Package> + <Private-Package> + org.apache.felix.http.base.*, + org.apache.felix.http.jetty.*, + org.eclipse.jetty.version + </Private-Package> + <Conditional-Package> + org.apache.commons.* + </Conditional-Package> + <Import-Package> + org.eclipse.jetty.client;resolution:=optional, + sun.misc;resolution:=optional, + sun.nio.ch;resolution:=optional, + javax.imageio;resolution:=optional, + javax.sql;resolution:=optional, + org.ietf.jgss;resolution:=optional, + org.osgi.service.cm;resolution:=optional;version="[1.3,2)", + org.osgi.service.event;resolution:=optional;version="[1.2,2)", + org.osgi.service.log;resolution:=optional;version="[1.3,2)", + org.osgi.service.metatype;resolution:=optional;version="[1.1,2)", + org.osgi.service.useradmin;resolution:=optional;version="[1.1,2)", + org.osgi.service.http;version="[1.2.1,1.3)", + org.osgi.service.http.context;version="[1.1,1.2)", + org.osgi.service.http.runtime;version="[1.1,1.2)", + org.osgi.service.http.runtime.dto;version="[1.1,1.2)", + org.slf4j;version="[1.0,3.0)", + * + </Import-Package> + <DynamicImport-Package> + org.osgi.service.cm;version="[1.3,2)", + org.osgi.service.event;version="[1.2,2)", + org.osgi.service.log;version="[1.3,2)", + org.osgi.service.metatype;version="[1.4,2)" + </DynamicImport-Package> + <Provide-Capability> + osgi.implementation;osgi.implementation="osgi.http";version:Version="1.1"; + uses:="javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard", + osgi.implementation;osgi.implementation="osgi.http";version:Version="2.0"; + uses:="jakarta.servlet,jakarta.servlet.http,org.osgi.service.servlet.context,org.osgi.service.servlet.whiteboard", + osgi.service;objectClass:List<String>="org.osgi.service.servlet.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.servlet.runtime,org.osgi.service.servlet.runtime.dto", + osgi.service;objectClass:List<String>="org.osgi.service.http.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto", + osgi.service;objectClass:List<String>="org.osgi.service.http.HttpService"; + uses:="org.osgi.service.http", + osgi.serviceloader;osgi.serviceloader="org.eclipse.jetty.http.HttpFieldPreEncoder" + </Provide-Capability> + <Require-Capability> + osgi.contract;filter:="(&(osgi.contract=JavaServlet)(version=4.0))", + osgi.contract;filter:="(&(osgi.contract=JakartaServlet)(version=6.0))", + osgi.extender;filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional, + osgi.extender;filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, + osgi.serviceloader;filter:="(osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder)";resolution:=optional;cardinality:=multiple, + osgi.serviceloader;filter:="(osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server)";resolution:=optional;cardinality:=multiple + </Require-Capability> + <Include-Resource> + {maven-resources},${project.build.directory}/serviceloader-resources + </Include-Resource> + <_removeheaders> + Private-Package,Conditional-Package,Include-Resource + </_removeheaders> + </instructions> + </configuration> + </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> @@ -366,11 +628,6 @@ <artifactId>jetty-ee10-servlet</artifactId> <version>${jetty.version}</version> </dependency> - <dependency> - <groupId>org.eclipse.jetty.ee10.websocket</groupId> - <artifactId>jetty-ee10-websocket-jetty-server</artifactId> - <version>${jetty.version}</version> - </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> @@ -417,19 +674,27 @@ <version>${jetty.version}</version> </dependency> <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-session</artifactId> + <groupId>org.eclipse.jetty.ee10.websocket</groupId> + <artifactId>jetty-ee10-websocket-jakarta-server</artifactId> + <version>${jetty.version}</version> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.eclipse.jetty.ee10.websocket</groupId> + <artifactId>jetty-ee10-websocket-jetty-server</artifactId> <version>${jetty.version}</version> + <optional>true</optional> </dependency> <dependency> - <groupId>org.eclipse.jetty.websocket</groupId> - <artifactId>jetty-websocket-jetty-api</artifactId> - <version>${jetty.version}</version> + <groupId>org.eclipse.jetty.websocket</groupId> + <artifactId>jetty-websocket-jetty-server</artifactId> + <version>${jetty.version}</version> + <optional>true</optional> </dependency> <dependency> - <groupId>org.eclipse.jetty.websocket</groupId> - <artifactId>jetty-websocket-jetty-server</artifactId> - <version>${jetty.version}</version> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-session</artifactId> + <version>${jetty.version}</version> </dependency> <dependency> <groupId>org.osgi</groupId> @@ -463,6 +728,7 @@ <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.5</version> + <optional>true</optional> </dependency> <dependency> <groupId>commons-io</groupId> @@ -488,5 +754,76 @@ <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>jetty-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/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java index 917e334132..f12c62641c 100644 --- a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java +++ b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java @@ -489,6 +489,17 @@ 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_EE10_WEBSOCKET_ENABLE, + "Enable Jakarta EE10 standard WebSocket support", + "Whether to enable jakarta EE10 standard WebSocket support. Default is false.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JAKARTA_EE10_WEBSOCKET_ENABLE))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_EE10_WEBSOCKET_ENABLE, + "Enable Jetty EE10 specific WebSocket support", + "Whether to enable jetty EE10 specific WebSocket support. Default is false.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_EE10_WEBSOCKET_ENABLE))); return new ObjectClassDefinition() { diff --git a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java index 05e6c7f941..cb61ae0b8b 100644 --- a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java +++ b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java @@ -271,6 +271,12 @@ 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 EE10 APIs provided by Jakarta WebSocket 2.1 */ + public static final String FELIX_JAKARTA_EE10_WEBSOCKET_ENABLE = "org.apache.felix.jakarta.ee10.websocket.enable"; + + /** Felix specific property to control whether to enable they Jetty-specific EE10 WebSocket APIs */ + public static final String FELIX_JETTY_EE10_WEBSOCKET_ENABLE = "org.apache.felix.jetty.ee10.websocket.enable"; + private static String validateContextPath(String ctxPath) { // undefined, empty, or root context path @@ -677,6 +683,22 @@ public final class JettyConfig return getLongProperty(FELIX_JETTY_STOP_TIMEOUT, -1l); } + /** + * Returns <code>true</code> if jakarta EE10 websocket is configured to be used ( + * {@link #FELIX_JAKARTA_EE10_WEBSOCKET_ENABLE}) + */ + public boolean isUseJakartaEE10Websocket() { + return getBooleanProperty(FELIX_JAKARTA_EE10_WEBSOCKET_ENABLE, false); + } + + /** + * Returns <code>true</code> if jetty websocket is configured to be used ( + * {@link #FELIX_JETTY_EE10_WEBSOCKET_ENABLE}) + */ + public boolean isUseJettyEE10Websocket() { + return getBooleanProperty(FELIX_JETTY_EE10_WEBSOCKET_ENABLE, false); + } + public void reset() { update(null); diff --git a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java index f75a6af3d8..ea2eca3d32 100644 --- a/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java +++ b/http/jetty12/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java @@ -253,7 +253,7 @@ public final class JettyService loginService.setUserStore(new UserStore()); this.server.addBean(loginService); - ServletContextHandler context = new ServletContextHandler(this.config.getContextPath(), + ServletContextHandler context = new ServletContextHandler(this.config.getContextPath(), ServletContextHandler.SESSIONS); this.parent = new ContextHandlerCollection(context); @@ -309,8 +309,18 @@ public final class JettyService this.server.setStopTimeout(this.config.getStopTimeout()); } + if (this.config.isUseJettyEE10Websocket()) { + maybeInitializeJettyEE10Websocket(context); + } + + if (this.config.isUseJakartaEE10Websocket()) { + maybeInitializeJakartaEE10Websocket(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, @@ -478,6 +488,80 @@ public final class JettyService return startConnector(connector); } + /** + * Initialize the jakarta EE10 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 maybeInitializeJakartaEE10Websocket(ServletContextHandler handler) { + if (isClassNameVisible("org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer")) { + // Ensure that JakartaWebSocketServletContainerInitializer is initialized, + // to setup the ServerContainer for this web application context. + org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer.configure(handler, null); + SystemLogger.LOGGER.info("Jakarta WebSocket EE10 servlet container initialized"); + } else { + SystemLogger.LOGGER.warn("Failed to initialize jakarta EE10 standard websocket support since the initializer class was not found. " + + "Check if the jetty-ee10-websocket-jakarta-server bundle is deployed."); + } + } + + /** + * Initialize the jetty EE10 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 maybeInitializeJettyEE10Websocket(ServletContextHandler handler) { + if (isClassNameVisible("org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer")) { + // Ensure that JettyWebSocketServletContainerInitializer is initialized, + // to setup the JettyWebSocketServerContainer for this web application context. + org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer.configure(handler, null); + SystemLogger.LOGGER.info("Jetty WebSocket EE10 servlet container initialized"); + } else { + SystemLogger.LOGGER.warn("Failed to initialize jetty EE10 specific websocket support since the initializer class was not found. " + + "Check if the jetty-ee10-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.isUseJettyEE10Websocket() && + isClassNameVisible("org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer")) { + String attribute = org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer.JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE; + this.controller.setAttributeSharedServletContext(attribute, context.getServletContext().getAttribute(attribute)); + } + if (this.config.isUseJakartaEE10Websocket() && + isClassNameVisible("org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer")) { + String attribute = org.eclipse.jetty.ee10.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/jetty12/src/test/java/org/apache/felix/http/jetty/it/AbstractJettyTestSupport.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/AbstractJettyTestSupport.java new file mode 100644 index 0000000000..5da84b6d4f --- /dev/null +++ b/http/jetty12/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") + ); + } +} \ No newline at end of file diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JakartaEE10SpecificWebsocketIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JakartaEE10SpecificWebsocketIT.java new file mode 100644 index 0000000000..1f19a4ad39 --- /dev/null +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JakartaEE10SpecificWebsocketIT.java @@ -0,0 +1,211 @@ +/* + * 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 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; + +import org.awaitility.Awaitility; +import org.eclipse.jetty.ee10.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; + +/** + * + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class JakartaEE10SpecificWebsocketIT extends AbstractJettyTestSupport { + + @Inject + protected BundleContext bundleContext; + + @Override + protected Option[] additionalOptions() throws IOException { + String jettyVersion = System.getProperty("jetty.version", "12.0.8"); + return new Option[]{ + spifly(), + + // bundles for the server side + mavenBundle().groupId("jakarta.websocket").artifactId("jakarta.websocket-api").version("2.1.1"), + mavenBundle().groupId("jakarta.websocket").artifactId("jakarta.websocket-client-api").version("2.1.1"), + 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.ee10").artifactId("jetty-ee10-webapp").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-core-client").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-core-common").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-core-server").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.ee10.websocket").artifactId("jetty-ee10-websocket-jakarta-client").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.ee10.websocket").artifactId("jetty-ee10-websocket-jakarta-common").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.ee10.websocket").artifactId("jetty-ee10-websocket-jakarta-server").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.ee10.websocket").artifactId("jetty-ee10-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.ee10.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); + + // Lookup the ServletContext for the context path where the websocket server is attached. + ServletContext servletContext = config.getServletContext(); + + // Retrieve the ServerContainer from the ServletContext attributes. + 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); + } + } + +} \ No newline at end of file diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyEE10SpecificWebsocketIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyEE10SpecificWebsocketIT.java new file mode 100644 index 0000000000..72fe774fcf --- /dev/null +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettyEE10SpecificWebsocketIT.java @@ -0,0 +1,209 @@ +/* + * 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 jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; + +import org.awaitility.Awaitility; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.api.Callback; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +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; + +/** + * + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class JettyEE10SpecificWebsocketIT extends AbstractJettyTestSupport { + + @Inject + protected BundleContext bundleContext; + + @Override + protected Option[] additionalOptions() throws IOException { + String jettyVersion = System.getProperty("jetty.version", "12.0.8"); + return new Option[] { + spifly(), + + // bundles for the server side + mavenBundle().groupId("org.eclipse.jetty.ee10").artifactId("jetty-ee10-webapp").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-core-common").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-core-server").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-jetty-api").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-jetty-common").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-websocket-jetty-server").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.ee10.websocket").artifactId("jetty-ee10-websocket-servlet").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.ee10.websocket").artifactId("jetty-ee10-websocket-jetty-server").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("jetty-websocket-core-client").version(jettyVersion), + mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("jetty-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.ee10.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); + + // Lookup the ServletContext for the context path where the websocket server is attached. + ServletContext servletContext = config.getServletContext(); + + // Retrieve the JettyWebSocketServerContainer. + JettyWebSocketServerContainer container = JettyWebSocketServerContainer.getContainer(servletContext); + assertNotNull(container); + container.addMapping("/mywebsocket1", (upgradeRequest, upgradeResponse) -> new MyServerWebSocket()); + } + } + + /** + * WebSocket handler for the client side + */ + @WebSocket() + public static class MyClientWebSocket { + private Session session; + private String lastMessage; + + public String getLastMessage() { + return lastMessage; + } + + @OnWebSocketOpen + 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.sendText(msg, Callback.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() + 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.sendText(message, Callback.NOOP); + } + } + +} \ No newline at end of file diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/MissingWebsocketDependenciesIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/MissingWebsocketDependenciesIT.java new file mode 100644 index 0000000000..7bb6bf43fc --- /dev/null +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/MissingWebsocketDependenciesIT.java @@ -0,0 +1,91 @@ +/* + * 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.ee10.websocket.enable", true) + .put("org.apache.felix.jakarta.ee10.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.jetty12[org.apache.felix.http]")); + + assertTrue(containsString(logFile, "org.apache.felix.http.jetty12[org.apache.felix.http] : Failed to " + + "initialize jetty EE10 specific websocket support since the initializer class was not found. " + + "Check if the jetty-ee10-websocket-jetty-server bundle is deployed.")); + assertTrue(containsString(logFile, "org.apache.felix.http.jetty12[org.apache.felix.http] : Failed to " + + "initialize jakarta EE10 standard websocket support since the initializer class was not found. " + + "Check if the jetty-ee10-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)); + } + } + +} \ No newline at end of file diff --git a/http/pom.xml b/http/pom.xml index 8684e405ff..97f623fe7b 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -44,6 +44,7 @@ <module>inventoryprinter</module> <module>itest</module> <module>jetty</module> + <module>jetty12</module> <module>proxy</module> <module>samples/whiteboard</module> <module>servlet-api</module> diff --git a/http/samples/whiteboard/pom.xml b/http/samples/whiteboard/pom.xml index 4f40bff0ce..4f7b63d825 100644 --- a/http/samples/whiteboard/pom.xml +++ b/http/samples/whiteboard/pom.xml @@ -32,16 +32,16 @@ <version>3.0.0-SNAPSHOT</version> <packaging>bundle</packaging> - <properties> - <jetty.version>12.0.8</jetty.version> - </properties> - <scm> <connection>scm:git:https://github.com/apache/felix-dev.git</connection> <developerConnection>scm:git:https://github.com/apache/felix-dev.git</developerConnection> <url>https://gitbox.apache.org/repos/asf?p=felix-dev.git</url> </scm> + <properties> + <jetty.version>12.0.8</jetty.version> + </properties> + <build> <plugins> <plugin> @@ -105,6 +105,18 @@ <version>3.0.0</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <version>${jetty.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty.ee10</groupId> + <artifactId>jetty-ee10-servlet</artifactId> + <version>${jetty.version}</version> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.eclipse.jetty.ee10.websocket</groupId> <artifactId>jetty-ee10-websocket-jetty-server</artifactId> diff --git a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java index 8e4346641c..6963791264 100644 --- a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java +++ b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/Activator.java @@ -77,21 +77,40 @@ public final class Activator context.registerService(Filter.class, filter2, filter2Props); /** - * Register a WebSocket servlet on /filtersample/websocket/*. + * Register WebSocket servlet on /websocketservlet/*. + * Do note that the path the servlet is registered to is not reflected in the WebSocket URL. + * This is due to the way of registering the WebSocket code. * In the Chrome Console, this snippet can be used to send a message to the WebSocket: * - * const websocket = new WebSocket("ws://localhost:8080/filtersample/websocket/example"); - * websocket.send("test"); + * const websocket = new WebSocket("ws://localhost:8080/websocket/example"); + * websocket.send("test from websocket"); * - * This will log "test" to the stdout. + * This will log "test from websocket" to the stdout. */ - final TestWebSocketServlet webSocketServlet = new TestWebSocketServlet("websocketservlet1"); + final TestWebSocketServlet webSocketServlet = new TestWebSocketServlet("websocket1"); final Dictionary<String, Object> webSocketServletProps = new Hashtable<>(); - webSocketServletProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/websocket/*"); + webSocketServletProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/websocketservlet/*"); webSocketServletProps.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=filtersample)"); context.registerService(Servlet.class, webSocketServlet, webSocketServletProps); + /** + * Register another WebSocket servlet on /websocketservlet2/*. + * Do note that the path the servlet is registered to _is_ reflected in the WebSocket URL. + * This is due to the way of registering the WebSocket code. + * In the Chrome Console, this snippet can be used to send a message to the WebSocket: + * + * const websocket = new WebSocket("ws://localhost:8080/filtersample/websocketservlet2/example"); + * websocket.send("test from websocket"); + * + * This will log "test from websocket" to the stdout. + */ + final TestWebSocketServletAlternative webSocketServlet2 = new TestWebSocketServletAlternative("websocket2"); + final Dictionary<String, Object> webSocketServletProps2 = new Hashtable<>(); + webSocketServletProps2.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/websocketservlet2/*"); + webSocketServletProps2.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, + "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=filtersample)"); + context.registerService(Servlet.class, webSocketServlet2, webSocketServletProps2); } @Override diff --git a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java index a8ed768291..55af0f1863 100644 --- a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java +++ b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java @@ -17,82 +17,32 @@ package org.apache.felix.http.samples.whiteboard; import java.io.IOException; -import java.lang.reflect.Proxy; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; -import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; -import org.eclipse.jetty.ee10.servlet.ServletContextHandler; -import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet; -import org.eclipse.jetty.ee10.websocket.server.internal.JettyServerFrameHandlerFactory; -import org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter; -import org.eclipse.jetty.websocket.core.server.WebSocketMappings; -import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; /** * Abstract class that hides all Jetty Websocket specifics and provides a way for the developer to focus on the actual WebSocket implementation. - * @author paulrutters */ public abstract class FelixJettyWebSocketServlet extends JettyWebSocketServlet { - private final AtomicBoolean myFirstInitCall = new AtomicBoolean(true); private final CountDownLatch myInitBarrier = new CountDownLatch(1); - private ServletContext myProxiedContext; - private ServletContextHandler myServletContextHandler; - @Override - public void init() throws ServletException { - // Init, delaying init call until service method is called... + public final void init() { + // nothing, see delayed init below in service method + // this is a workaround as stated in https://issues.apache.org/jira/browse/FELIX-5310 } @Override - public void destroy() { - // only call destroy when the servlet has been initialized - if (!myFirstInitCall.get()) { - // This is required because WebSocketServlet needs to have it's destroy() method called as well - // Causes NPE otherwise when calling an WS endpoint - super.destroy(); - } - } - - - // This is a workaround required for WebSockets to work in Jetty12, see - // https://www.eclipse.org/forums/index.php/t/1110140/ - @Override - public synchronized ServletContext getServletContext() { - if (myProxiedContext == null) { - myProxiedContext = (ServletContext) Proxy.newProxyInstance(JettyWebSocketServlet.class.getClassLoader(), - new Class[]{ServletContext.class}, (proxy, method, methodArgs) -> { - final ServletContext osgiServletContext = super.getServletContext(); - if (!"getAttribute".equals(method.getName())) { - return method.invoke(osgiServletContext, methodArgs); - } - - final String name = (String) methodArgs[0]; - Object value = osgiServletContext.getAttribute(name); - if (value == null && myProxiedContext != null) { - final ServletContext jettyServletContext = myServletContextHandler.getServletContext(); - value = jettyServletContext.getAttribute(name); - } - return value; - }); - } - - return myProxiedContext; - } - - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + public void service(final ServletRequest req, final ServletResponse res) throws ServletException, IOException { if (myFirstInitCall.compareAndSet(true, false)) { try { - delayedInit(); - } catch (Exception e) { - System.err.println("Error delayed init: " + e.getMessage()); + super.init(); } finally { myInitBarrier.countDown(); } @@ -104,42 +54,19 @@ public abstract class FelixJettyWebSocketServlet extends JettyWebSocketServlet { } } - // Call JettyWebSocketServlet service method to handle upgrade requests - super.service(req, resp); - } - - private void delayedInit() throws ServletException { - // Make sure WebSockets are enabled in Jetty12 - ensureWebSocketsInitialized(); - - // Overide the TCCL so that the internal factory can be found - // Jetty tries to use ServiceLoader, and their fallback is to - // use TCCL, it would be better if we could provide a loader... - final Thread currentThread = Thread.currentThread(); - final ClassLoader tccl = currentThread.getContextClassLoader(); - currentThread.setContextClassLoader(JettyWebSocketServlet.class.getClassLoader()); - try { - super.init(); - } finally { - currentThread.setContextClassLoader(tccl); - } + super.service(req, res); } - private void ensureWebSocketsInitialized() { - final ServletContext osgiServletContext = getServletContext(); - myServletContextHandler = ServletContextHandler.getServletContextHandler(osgiServletContext, "WebSockets"); - - final JettyWebSocketServerContainer serverContainer = JettyWebSocketServerContainer - .getContainer(osgiServletContext); - if (serverContainer == null) { - // Ensure WebSocket components are initialized in Jetty12 - final ServletContext jettyServletContext = myServletContextHandler.getServletContext(); - WebSocketServerComponents.ensureWebSocketComponents(myServletContextHandler.getServer(), - myServletContextHandler); - WebSocketUpgradeFilter.ensureFilter(jettyServletContext); - WebSocketMappings.ensureMappings(myServletContextHandler); - JettyServerFrameHandlerFactory.getFactory(jettyServletContext); - JettyWebSocketServerContainer.ensureContainer(jettyServletContext); + /** + * Cleanup method. + */ + @Override + public final void destroy() { + // only call destroy when the servlet has been initialized + if (!myFirstInitCall.get()) { + // This is required because WebSocketServlet needs to have it's destroy() method called as well + // Causes NPE otherwise when calling an WS endpoint + super.destroy(); } } } diff --git a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java index b09b7f6bbb..7408c319db 100644 --- a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java +++ b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java @@ -17,9 +17,11 @@ package org.apache.felix.http.samples.whiteboard; import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; -import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; @@ -28,29 +30,37 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -public class TestWebSocketServlet extends FelixJettyWebSocketServlet { +/** + * Example of a WebSocket servlet that uses the Jetty WebSocket API. + * It does not respect the path this servlet is registered to, but requires no further workarounds. + * Setting `org.apache.felix.jetty.ee10.websocket.enable=true` is enough. + */ +public class TestWebSocketServlet extends HttpServlet { private final String name; public TestWebSocketServlet(String name) { this.name = name; } + private void doLog(String message) { + System.out.println("## [" + this.name + "] " + message); + } + + @Override public void init(ServletConfig config) throws ServletException { doLog("Init with config [" + config + "]"); super.init(config); - } - private void doLog(String message) { - System.out.println("## [" + this.name + "] " + message); - } + // Lookup the ServletContext for the context path where the websocket server is attached. + ServletContext servletContext = config.getServletContext(); - @Override - protected void configure(JettyWebSocketServletFactory jettyWebSocketServletFactory) { - doLog("Configuring WebSocket factory"); - jettyWebSocketServletFactory.register(TestWebSocket.class); + // Retrieve the JettyWebSocketServerContainer. + JettyWebSocketServerContainer container = JettyWebSocketServerContainer.getContainer(servletContext); + container.addMapping("/websocket/*", (upgradeRequest, upgradeResponse) -> new TestWebSocket()); } + @WebSocket public static class TestWebSocket { @OnWebSocketMessage @@ -80,4 +90,4 @@ public class TestWebSocketServlet extends FelixJettyWebSocketServlet { System.out.println("## [" + this.getClass() + "] " + message); } } -} +} \ No newline at end of file diff --git a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServletAlternative.java similarity index 82% copy from http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java copy to http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServletAlternative.java index b09b7f6bbb..df980c4ea2 100644 --- a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServlet.java +++ b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServletAlternative.java @@ -28,27 +28,31 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -public class TestWebSocketServlet extends FelixJettyWebSocketServlet { +/** + * Example of a WebSocket servlet that uses the Jetty WebSocket API, and is registered by extending JettyWebSocketServlet. + * It does respect the path this servlet is registered to, but requires a further workaround. See FelixJettyWebSocketServlet. + * Requires setting `org.apache.felix.jetty.ee10.websocket.enable=true`. + */ +public class TestWebSocketServletAlternative extends FelixJettyWebSocketServlet { private final String name; - public TestWebSocketServlet(String name) { + public TestWebSocketServletAlternative(String name) { this.name = name; } + private void doLog(String message) { + System.out.println("## [" + this.name + "] " + message); + } + @Override public void init(ServletConfig config) throws ServletException { doLog("Init with config [" + config + "]"); super.init(config); } - private void doLog(String message) { - System.out.println("## [" + this.name + "] " + message); - } - @Override - protected void configure(JettyWebSocketServletFactory jettyWebSocketServletFactory) { - doLog("Configuring WebSocket factory"); - jettyWebSocketServletFactory.register(TestWebSocket.class); + protected void configure(JettyWebSocketServletFactory factory) { + factory.register(TestWebSocket.class); } @WebSocket @@ -80,4 +84,4 @@ public class TestWebSocketServlet extends FelixJettyWebSocketServlet { System.out.println("## [" + this.getClass() + "] " + message); } } -} +} \ No newline at end of file