diff --git a/http/README.md b/http/README.md index 8f3741b859..ed71f528d2 100644 --- a/http/README.md +++ b/http/README.md @@ -442,6 +442,7 @@ properties can be used (some legacy property names still exist but are not docum | `org.apache.felix.jetty.websocket.enable` | Enables Jetty websocket support. Default is false. | | `org.apache.felix.http.jetty.threadpool.max` | The maximum number of threads in the Jetty thread pool. Default is unlimited. Works for both platform threads and virtual threads (Jetty 12 only). | | `org.apache.felix.http.jetty.virtualthreads.enable` | Enables using virtual threads in Jetty 12 (JDK 21 required). Default is false. When enabled, `org.apache.felix.http.jetty.threadpool.max` is used for a bounded virtual thread pool. | + ### Multiple Servers It is possible to configure several Http Services, each running on a different port. The first service can be configured as outlined above using the service PID for `"org.apache.felix.http"`. Additional servers can be configured through OSGi factory configurations using `"org.apache.felix.http"` as the factory PID. The properties for the configuration are outlined as above. diff --git a/http/base/pom.xml b/http/base/pom.xml index bd6eeea198..1ae70a29af 100644 --- a/http/base/pom.xml +++ b/http/base/pom.xml @@ -157,5 +157,11 @@ 5.7.0 test + + org.eclipse.jetty.ee10.websocket + jetty-ee10-websocket-jetty-server + 12.0.16 + test + diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java index 96439f5b1a..e8658faa84 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java @@ -145,6 +145,7 @@ public void register(@NotNull final ServletContext containerContext, @NotNull fi */ public void setAttributeSharedServletContext(String key, Object value) { this.whiteboardManager.setAttributeSharedServletContext(key, value); + this.httpServiceFactory.setAttributeSharedServletContext(key, value); } /** diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java index 255ad55551..d84a0e120e 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java @@ -26,7 +26,7 @@ /** * Servlet handler for servlets registered through the http service. */ -public final class HttpServiceServletHandler extends ServletHandler +public class HttpServiceServletHandler extends ServletHandler { /** * New handler diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceWebSocketServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceWebSocketServletHandler.java new file mode 100644 index 0000000000..8d99962538 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceWebSocketServletHandler.java @@ -0,0 +1,65 @@ +/* + * 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.base.internal.handler; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.ServletInfo; + +/** + * Servlet handler for servlets extending JettyWebSocketServlet registered through the http service. + */ +public final class HttpServiceWebSocketServletHandler extends HttpServiceServletHandler +{ + private final WebSocketHandler webSocketHandler; + + public HttpServiceWebSocketServletHandler(final ExtServletContext context, + final ServletInfo servletInfo, + final javax.servlet.Servlet servlet) + { + super(context, servletInfo, servlet); + this.webSocketHandler = new WebSocketHandler(this); + } + + @Override + public int init() { + if (webSocketHandler.shouldInit()) { + return super.init(); + } + // do nothing, delay init until first service call + return -1; + } + + @Override + public void handle(ServletRequest req, ServletResponse res) throws ServletException, IOException { + this.webSocketHandler.lazyInit(); + super.handle(req, res); + } + + @Override + public boolean destroy() { + if (webSocketHandler.shouldDestroy()) { + return super.destroy(); + } + return false; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java index 5a9c97a0ac..4254ce6a93 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java @@ -180,7 +180,7 @@ public int init() catch (final Exception e) { SystemLogger.LOGGER.error(SystemLogger.formatMessage(this.getServletInfo().getServiceReference(), - "Error during calling init() on servlet ".concat(this.servletInfo.getClassName(this.servlet))), + "Error during calling init() on servlet ".concat(this.servletInfo.getClassName(this.servlet))), e); return DTOConstants.FAILURE_REASON_EXCEPTION_ON_INIT; } @@ -188,7 +188,6 @@ public int init() return -1; } - public boolean destroy() { if (this.servlet == null) diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WebSocketHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WebSocketHandler.java new file mode 100644 index 0000000000..89b58d7c3f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WebSocketHandler.java @@ -0,0 +1,115 @@ +/* + * 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.base.internal.handler; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.felix.http.base.internal.logger.SystemLogger; + +/** + * Class that handles initialization for servlets extending JettyWebSocketServlet. + */ +public final class WebSocketHandler { + // The Jetty class used for Jetty WebSocket servlets + private static final String JETTY_WEB_SOCKET_SERVLET_CLASS = "JettyWebSocketServlet"; + + private final AtomicBoolean lazyFirstInitCall = new AtomicBoolean(true); + private final CountDownLatch initBarrier = new CountDownLatch(1); + private final ServletHandler servletHandler; + + public WebSocketHandler(ServletHandler servletHandler) { + this.servletHandler = servletHandler; + } + + /* + * Lazy initialization of the servlet. + * Will only be called once for each servlet instance and is thread-safe. + */ + public void lazyInit() { + if (lazyFirstInitCall.compareAndSet(true, false)) { + try { + this.servletHandler.init(); + } catch (final Exception e) { + SystemLogger.LOGGER.error(SystemLogger.formatMessage( + this.servletHandler.getServletInfo().getServiceReference(), + "Error calling init() lazy on servlet ".concat( + this.servletHandler.getServletInfo().getClassName(this.servletHandler.getServlet()))), e); + } finally { + initBarrier.countDown(); + } + } else { + // already initialized, await the first initialization + try { + initBarrier.await(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Returns true if the servlet should be initialized, false otherwise. + * @return true if the servlet should be initialized, false otherwise + */ + public boolean shouldInit() { + return !lazyFirstInitCall.get() && initBarrier.getCount() > 0; + } + + /** + * Returns true if the servlet was initialized earlier, false otherwise. + * @return true if the servlet should be destroyed, false otherwise + */ + public boolean shouldDestroy() { + if (!lazyFirstInitCall.get()){ + try { + initBarrier.await(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + return true; + } + return false; + } + + /** + * Check if the servlet is a JettyWebSocketServlet. + * JettyWebSocket classes are handled differently due to FELIX-6746. + * @param servlet the servlet to check + * @return true if the servlet is a JettyWebSocketServlet, false otherwise + */ + public static boolean isJettyWebSocketServlet(Object servlet) { + final Class superClass = servlet.getClass().getSuperclass(); + SystemLogger.LOGGER.debug("Checking if the servlet is a JettyWebSocketServlet: '" + superClass.getSimpleName() + "'"); + + // Now check if the servlet class extends 'JettyWebSocketServlet' + boolean isJettyWebSocketServlet = superClass.getSimpleName().endsWith(JETTY_WEB_SOCKET_SERVLET_CLASS); + if (!isJettyWebSocketServlet) { + // Recurse through the wrapped servlets, in case of double-wrapping + if (servlet instanceof org.apache.felix.http.jakartawrappers.ServletWrapper) { + final javax.servlet.Servlet wrappedServlet = ((org.apache.felix.http.jakartawrappers.ServletWrapper) servlet).getServlet(); + return isJettyWebSocketServlet(wrappedServlet); + } else if (servlet instanceof org.apache.felix.http.javaxwrappers.ServletWrapper) { + final jakarta.servlet.Servlet wrappedServlet = ((org.apache.felix.http.javaxwrappers.ServletWrapper) servlet).getServlet(); + return isJettyWebSocketServlet(wrappedServlet); + } + } + return isJettyWebSocketServlet; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java index 5fae620ab5..8427da5535 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java @@ -29,7 +29,7 @@ /** * Servlet handler for servlets registered through the http whiteboard. */ -public final class WhiteboardServletHandler extends ServletHandler +public class WhiteboardServletHandler extends ServletHandler { private final BundleContext bundleContext; diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardWebSocketServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardWebSocketServletHandler.java new file mode 100644 index 0000000000..dba9de191e --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardWebSocketServletHandler.java @@ -0,0 +1,76 @@ +/* + * 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.base.internal.handler; + +import java.io.FilePermission; +import java.io.IOException; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.servlet.runtime.dto.DTOConstants; + +/** + * Servlet handler for servlets extending JettyWebSocketServlet registered through the http whiteboard. + */ +public final class WhiteboardWebSocketServletHandler extends WhiteboardServletHandler +{ + private final WebSocketHandler webSocketHandler; + + public WhiteboardWebSocketServletHandler(final long contextServiceId, + final ExtServletContext context, + final ServletInfo servletInfo, + final BundleContext contextBundleContext, + final Bundle registeringBundle, + final Bundle httpWhiteboardBundle, + final Object servlet) + { + super(contextServiceId, context, servletInfo, contextBundleContext, registeringBundle, httpWhiteboardBundle); + this.webSocketHandler = new WebSocketHandler(this); + this.setServlet((Servlet) servlet); + } + + @Override + public int init() { + if (webSocketHandler.shouldInit()) { + return super.init(); + } + // do nothing, delay init until first service call + return -1; + } + + @Override + public void handle(ServletRequest req, ServletResponse res) throws ServletException, IOException { + this.webSocketHandler.lazyInit(); + super.handle(req, res); + } + + @Override + public boolean destroy() { + if (webSocketHandler.shouldDestroy()) { + return super.destroy(); + } + return false; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java index 7ff37e0309..c5f7b1d8a3 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java @@ -16,10 +16,14 @@ */ package org.apache.felix.http.base.internal.service; +import java.util.HashMap; import java.util.Hashtable; +import java.util.Map; +import org.apache.felix.http.base.internal.logger.SystemLogger; import org.apache.felix.http.base.internal.registry.HandlerRegistry; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; @@ -76,6 +80,7 @@ public final class HttpServiceFactory private final HandlerRegistry handlerRegistry; private volatile SharedHttpServiceImpl sharedHttpService; + private volatile Map attributesForSharedContext = new HashMap<>(); public HttpServiceFactory(final BundleContext bundleContext, final HandlerRegistry handlerRegistry) @@ -101,6 +106,7 @@ public void start(final ServletContext context, this.context = context; this.sharedHttpService = new SharedHttpServiceImpl(handlerRegistry); + this.sharedHttpService.setSharedContextAttributes(attributesForSharedContext); this.active = true; this.httpServiceReg = bundleContext.registerService(HttpService.class, this, this.httpServiceProps); @@ -120,6 +126,7 @@ public void stop() this.sharedHttpService = null; this.httpServiceProps.clear(); + this.attributesForSharedContext.clear(); } @Override @@ -160,6 +167,11 @@ public long getHttpServiceServiceId() return (Long) this.httpServiceReg.getReference().getProperty(Constants.SERVICE_ID); } + public void setAttributeSharedServletContext(String key, Object value) { + SystemLogger.LOGGER.info("HttpServiceFactory: Storing attribute for shared servlet context. Key '{}', value: '{}'", key, value); + this.attributesForSharedContext.put(key, value); + } + private boolean getBoolean(final String property) { String prop = this.bundleContext.getProperty(property); diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java index d96c656375..cb7ed32ead 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java @@ -16,13 +16,17 @@ */ package org.apache.felix.http.base.internal.service; +import static org.apache.felix.http.base.internal.handler.WebSocketHandler.isJettyWebSocketServlet; + import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.apache.felix.http.base.internal.context.ExtServletContext; import org.apache.felix.http.base.internal.handler.HttpServiceServletHandler; +import org.apache.felix.http.base.internal.handler.HttpServiceWebSocketServletHandler; import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.logger.SystemLogger; import org.apache.felix.http.base.internal.registry.HandlerRegistry; import org.apache.felix.http.base.internal.runtime.ServletInfo; import org.apache.felix.http.jakartawrappers.ServletWrapper; @@ -38,6 +42,7 @@ public final class SharedHttpServiceImpl private final HandlerRegistry handlerRegistry; private final Map aliasMap = new HashMap<>(); + private Map attributesForSharedContext; /** * Create a new implementation @@ -67,7 +72,10 @@ public void registerServlet(@NotNull final String alias, @NotNull final javax.servlet.Servlet servlet, @NotNull final ServletInfo servletInfo) throws javax.servlet.ServletException, NamespaceException { - final ServletHandler handler = new HttpServiceServletHandler(httpContext, servletInfo, servlet); + final ServletHandler handler = getServletHandler(httpContext, servlet, servletInfo); + + // Track properties from shared context + setAttributes(httpContext); synchronized (this.aliasMap) { @@ -81,6 +89,19 @@ public void registerServlet(@NotNull final String alias, } } + @NotNull + private static HttpServiceServletHandler getServletHandler( + @NotNull ExtServletContext httpContext, + @NotNull javax.servlet.Servlet servlet, + @NotNull ServletInfo servletInfo) + { + if (isJettyWebSocketServlet(servlet)) + { + return new HttpServiceWebSocketServletHandler(httpContext, servletInfo, servlet); + } + return new HttpServiceServletHandler(httpContext, servletInfo, servlet); + } + /** * Unregister a servlet * @param alias The alias @@ -155,4 +176,23 @@ public void unregisterServlet(final javax.servlet.Servlet servlet) { return this.handlerRegistry; } + + public void setSharedContextAttributes(Map attributesForSharedContext) { + this.attributesForSharedContext = attributesForSharedContext; + } + + /** + * Set the stored attributes on the servlet context. + * @param context the servlet context + */ + private void setAttributes(ExtServletContext context) { + if (context != null && attributesForSharedContext != null) { + attributesForSharedContext.forEach((key, value) -> { + if (key != null && value != null) { + SystemLogger.LOGGER.info("SharedHttpServiceImpl: Shared context found, setting stored attribute key: '{}', value: '{}'", key, value); + context.setAttribute(key, value); + } + }); + } + } } diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java index cb48cb9a53..7f1cb5f4b6 100644 --- a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java @@ -16,6 +16,7 @@ */ package org.apache.felix.http.base.internal.whiteboard; +import static org.apache.felix.http.base.internal.handler.WebSocketHandler.isJettyWebSocketServlet; import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING; import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE; import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_UNKNOWN; @@ -39,7 +40,9 @@ import org.apache.felix.http.base.internal.handler.ListenerHandler; import org.apache.felix.http.base.internal.handler.PreprocessorHandler; import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.handler.WebSocketHandler; import org.apache.felix.http.base.internal.handler.WhiteboardServletHandler; +import org.apache.felix.http.base.internal.handler.WhiteboardWebSocketServletHandler; import org.apache.felix.http.base.internal.logger.SystemLogger; import org.apache.felix.http.base.internal.registry.EventListenerRegistry; import org.apache.felix.http.base.internal.registry.HandlerRegistry; @@ -416,7 +419,7 @@ private void setAttributes(@Nullable ServletContext context) { if (context != null) { attributesForSharedContext.forEach((key, value) -> { if (key != null && value != null) { - SystemLogger.LOGGER.info("Shared context found, setting stored attribute key: '{}', value: '{}'", key, value); + SystemLogger.LOGGER.info("WhiteboardManager: Shared context found, setting stored attribute key: '{}', value: '{}'", key, value); context.setAttribute(key, value); } }); @@ -726,13 +729,7 @@ private void registerWhiteboardService(final WhiteboardContextHandler handler, f } else { - final ServletHandler servletHandler = new WhiteboardServletHandler( - handler.getContextInfo().getServiceId(), - servletContext, - (ServletInfo)info, - handler.getBundleContext(), - info.getServiceReference().getBundle(), - this.httpBundleContext.getBundle()); + final ServletHandler servletHandler = getServletHandler(handler, info, servletContext); handler.getRegistry().registerServlet(servletHandler); } } @@ -805,6 +802,31 @@ else if ( info instanceof ListenerInfo ) } } + @NotNull + private WhiteboardServletHandler getServletHandler(WhiteboardContextHandler handler, + WhiteboardServiceInfo info, + ExtServletContext servletContext) + { + Object servlet = info.getService(handler.getBundleContext()); + if (isJettyWebSocketServlet(servlet)) + { + return new WhiteboardWebSocketServletHandler( + handler.getContextInfo().getServiceId(), + servletContext, + (ServletInfo) info, + handler.getBundleContext(), + info.getServiceReference().getBundle(), + this.httpBundleContext.getBundle(), servlet); + } + return new WhiteboardServletHandler( + handler.getContextInfo().getServiceId(), + servletContext, + (ServletInfo) info, + handler.getBundleContext(), + info.getServiceReference().getBundle(), + this.httpBundleContext.getBundle()); + } + /** * Unregister whiteboard service from the http service * @param handler Context handler @@ -981,7 +1003,7 @@ private void updateRuntimeChangeCount() * @param value attribute value */ public void setAttributeSharedServletContext(String key, Object value) { - SystemLogger.LOGGER.info("Storing attribute for shared servlet context. Key '{}', value: '{}'", key, value); + SystemLogger.LOGGER.info("WhiteboardManager: Storing attribute for shared servlet context. Key '{}', value: '{}'", key, value); this.attributesForSharedContext.put(key, value); } } diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/handler/WebSocketHandlerTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/WebSocketHandlerTest.java new file mode 100644 index 0000000000..e635a50f1d --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/WebSocketHandlerTest.java @@ -0,0 +1,52 @@ +/* + * 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.base.internal.handler; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.apache.felix.http.javaxwrappers.ServletWrapper; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet; +import org.junit.Before; +import org.junit.Test; + +public class WebSocketHandlerTest { + private javax.servlet.Servlet javaxServlet; + private jakarta.servlet.Servlet jakartaServlet; + private JettyWebSocketServlet jakartaWebSocketServlet; + + @Before + public void setUp() + { + this.javaxServlet = mock(javax.servlet.Servlet.class); + this.jakartaServlet = mock(jakarta.servlet.Servlet.class); + this.jakartaWebSocketServlet = mock(JettyWebSocketServlet.class); + } + + @Test + public void isJettyWebSocketServlet(){ + assertFalse(WebSocketHandler.isJettyWebSocketServlet(this.javaxServlet)); + assertFalse(WebSocketHandler.isJettyWebSocketServlet(this.jakartaServlet)); + + // See test scope dependency in pom.xml + assertTrue(WebSocketHandler.isJettyWebSocketServlet(this.jakartaWebSocketServlet)); + + // Also works with the wrapper + assertTrue(WebSocketHandler.isJettyWebSocketServlet(new ServletWrapper(this.jakartaWebSocketServlet))); + } +} diff --git a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettySpecificWebsocketIT.java b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettySpecificWebsocketIT.java index 63503f6e2b..27b33bc9d0 100644 --- a/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettySpecificWebsocketIT.java +++ b/http/jetty12/src/test/java/org/apache/felix/http/jetty/it/JettySpecificWebsocketIT.java @@ -20,10 +20,12 @@ import static org.junit.Assert.assertNotNull; import static org.ops4j.pax.exam.CoreOptions.mavenBundle; import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration; +import static org.osgi.service.servlet.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.Dictionary; import java.util.Hashtable; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -35,10 +37,13 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; +import org.apache.felix.http.javaxwrappers.ServletWrapper; 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.ee10.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; @@ -54,7 +59,6 @@ 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; /** * @@ -66,6 +70,9 @@ public class JettySpecificWebsocketIT extends AbstractJettyTestSupport { @Inject protected BundleContext bundleContext; + @Inject + protected HttpService httpService; + @Override protected Option[] additionalOptions() throws IOException { String jettyVersion = System.getProperty("jetty.version", JETTY_VERSION); @@ -100,14 +107,46 @@ protected Option felixHttpConfig(int httpPort) { .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" + HTTP_WHITEBOARD_SERVLET_PATTERN, "/mywebsocket1" ))); + assertWebSocketResponse("mywebsocket1"); + } + + @Test + public void testWebSocketServletWhiteboard() throws Exception { + final JettyWebSocketServlet webSocketServlet = new JettyWebSocketServlet() { + @Override + protected void configure(JettyWebSocketServletFactory jettyWebSocketServletFactory) { + jettyWebSocketServletFactory.register(MyServerWebSocket.class); + } + }; + final Dictionary props = new Hashtable<>(); + props.put(HTTP_WHITEBOARD_SERVLET_PATTERN, "/websocketservletwhiteboard"); + bundleContext.registerService(Servlet.class, webSocketServlet, props); + + assertWebSocketResponse("websocketservletwhiteboard"); + } + + @Test + public void testWebSocketServletHttpService() throws Exception { + final JettyWebSocketServlet webSocketServlet = new JettyWebSocketServlet() { + @Override + protected void configure(JettyWebSocketServletFactory jettyWebSocketServletFactory) { + jettyWebSocketServletFactory.register(MyServerWebSocket.class); + } + }; + + httpService.registerServlet("/websocketservlethttpservice", new ServletWrapper(webSocketServlet), null, null); + + assertWebSocketResponse("websocketservlethttpservice"); + } + + private void assertWebSocketResponse(String servletPath) throws Exception { HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(); HttpClient httpClient = new org.eclipse.jetty.client.HttpClient(transport); WebSocketClient webSocketClient = new WebSocketClient(httpClient); @@ -115,7 +154,7 @@ public void testWebSocketConversation() throws Exception { 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)); + URI destUri = new URI(String.format("ws://localhost:%d/%s", httpPort, servletPath)); MyClientWebSocket clientWebSocket = new MyClientWebSocket(); ClientUpgradeRequest request = new ClientUpgradeRequest(); 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 deleted file mode 100644 index 55af0f1863..0000000000 --- a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/FelixJettyWebSocketServlet.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.felix.http.samples.whiteboard; - -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; - -import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet; - -/** - * Abstract class that hides all Jetty Websocket specifics and provides a way for the developer to focus on the actual WebSocket implementation. - */ -public abstract class FelixJettyWebSocketServlet extends JettyWebSocketServlet { - private final AtomicBoolean myFirstInitCall = new AtomicBoolean(true); - private final CountDownLatch myInitBarrier = new CountDownLatch(1); - - 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 service(final ServletRequest req, final ServletResponse res) throws ServletException, IOException { - if (myFirstInitCall.compareAndSet(true, false)) { - try { - super.init(); - } finally { - myInitBarrier.countDown(); - } - } else { - try { - myInitBarrier.await(); - } catch (final InterruptedException e) { - throw new ServletException("Timed out waiting for initialisation", e); - } - } - - super.service(req, res); - } - - /** - * 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/TestWebSocketServletAlternative.java b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServletAlternative.java index e916a9a3ff..133fc1b4fc 100644 --- a/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServletAlternative.java +++ b/http/samples/whiteboard/src/main/java/org/apache/felix/http/samples/whiteboard/TestWebSocketServletAlternative.java @@ -19,6 +19,7 @@ import jakarta.servlet.ServletConfig; import jakarta.servlet.ServletException; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; @@ -30,10 +31,10 @@ /** * 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. + * It does respect the path this servlet is registered to. * Requires setting `org.apache.felix.jetty.websocket.enable=true`. */ -public class TestWebSocketServletAlternative extends FelixJettyWebSocketServlet { +public class TestWebSocketServletAlternative extends JettyWebSocketServlet { private final String name; public TestWebSocketServletAlternative(String name) {