From 5d85407c27c3fb50e38c97a0403d178eefe65252 Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Mon, 4 Jul 2022 17:46:28 +0200 Subject: [PATCH] Convert web-impl-osgi to WhiteBoard We are using OSGi R7, which has a very much workable HTTP Whiteboard specification. Rather than mucking with pax-web-api, use HTTP Whiteboard for WebContext implementation. This has the nice side-effect of working with any implementation, not only with pax-web. JIRA: AAA-225 Change-Id: I6387333b44dc9b6a40f909c3d3ceb75693193014 Signed-off-by: Robert Varga --- web/impl-osgi/pom.xml | 24 +- .../aaa/web/osgi/PaxWebServer.java | 207 ---------------- .../aaa/web/osgi/WhiteboardResource.java | 21 ++ .../osgi/WhiteboardServletContextHelper.java | 20 ++ .../aaa/web/osgi/WhiteboardWebServer.java | 229 ++++++++++++++++++ 5 files changed, 290 insertions(+), 211 deletions(-) delete mode 100644 web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/PaxWebServer.java create mode 100644 web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardResource.java create mode 100644 web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardServletContextHelper.java create mode 100644 web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardWebServer.java diff --git a/web/impl-osgi/pom.xml b/web/impl-osgi/pom.xml index 439d891dc..e2a3baf06 100644 --- a/web/impl-osgi/pom.xml +++ b/web/impl-osgi/pom.xml @@ -22,17 +22,25 @@ bundle + + com.google.guava + guava + + + javax.servlet + javax.servlet-api + org.opendaylight.aaa.web web-api - org.ops4j.pax.web - pax-web-api + org.opendaylight.yangtools + concepts - com.google.guava - guava + org.osgi + org.osgi.annotation.versioning org.osgi @@ -46,5 +54,13 @@ org.osgi org.osgi.service.component.annotations + + org.osgi + org.osgi.service.http + + + org.osgi + org.osgi.service.http.whiteboard + diff --git a/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/PaxWebServer.java b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/PaxWebServer.java deleted file mode 100644 index 1ec9e38c5..000000000 --- a/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/PaxWebServer.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) 2018 Red Hat, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.aaa.web.osgi; - -import static com.google.common.base.Verify.verifyNotNull; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.EventListener; -import java.util.List; -import java.util.Map; -import javax.servlet.Filter; -import javax.servlet.Servlet; -import javax.servlet.ServletContextListener; -import javax.servlet.ServletException; -import org.opendaylight.aaa.web.FilterDetails; -import org.opendaylight.aaa.web.ResourceDetails; -import org.opendaylight.aaa.web.ServletDetails; -import org.opendaylight.aaa.web.WebContext; -import org.opendaylight.aaa.web.WebServer; -import org.opendaylight.yangtools.concepts.AbstractRegistration; -import org.opendaylight.yangtools.concepts.Registration; -import org.ops4j.pax.web.service.WebContainer; -import org.ops4j.pax.web.service.WebContainerDTO; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.service.component.ComponentContext; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ServiceScope; -import org.osgi.service.http.HttpContext; -import org.osgi.service.http.HttpService; -import org.osgi.service.http.NamespaceException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link WebServer} (and {@link WebContext}) bridge implementation - * delegating to Pax Web WebContainer (which extends an OSGi {@link HttpService}). - * - * @author Michael Vorburger.ch - original author - * @author Tom Pantelis - added ServiceFactory to solve possible class loading issues in web components - * @author Robert Varga - reworked to use OSGi DS, which cuts the implementation down to bare bones. - */ -// FIXME: this really acts as an extender (note how we lookup in the context of target bundle) and should really be -// eliminated in favor of such -// FIXME: even if not, OSGi R7 is changing the picture and should allow us to work without this crud -// TODO write an IT (using Pax Exam) which tests this, re-use JettyLauncherTest -@Component(scope = ServiceScope.BUNDLE) -public final class PaxWebServer implements WebServer { - private static final Logger LOG = LoggerFactory.getLogger(PaxWebServer.class); - - // Global reference, acts as an activation guard - @Reference - WebContainer global = null; - - private ServiceReference ref; - private WebContainer local; - - @Override - public String getBaseURL() { - final WebContainerDTO details = local.getWebcontainerDTO(); - if (details.securePort != null && details.securePort > 0) { - return "https://" + details.listeningAddresses[0] + ":" + details.securePort; - } else { - return "http://" + details.listeningAddresses[0] + ":" + details.port; - } - } - - @Override - public Registration registerWebContext(final WebContext webContext) throws ServletException { - return new WebContextImpl(local, webContext); - } - - @Activate - void activate(final ComponentContext componentContext) { - final Bundle bundle = componentContext.getUsingBundle(); - final BundleContext bundleContext = bundle.getBundleContext(); - - ref = verifyNotNull(bundle.getBundleContext().getServiceReference(WebContainer.class), - "Failed to locate WebContext from %s", bundle); - local = verifyNotNull(bundleContext.getService(ref), "Failed to get WebContext in %s", bundle); - LOG.info("Activated WebServer instance for {}", bundleContext); - } - - @Deactivate - void deactivate(final ComponentContext componentContext) { - final Bundle bundle = componentContext.getUsingBundle(); - final BundleContext bundleContext = bundle.getBundleContext(); - local = null; - bundleContext.ungetService(ref); - ref = null; - LOG.info("Deactivated WebServer instance for {}", bundle); - } - - private static class WebContextImpl extends AbstractRegistration { - private final String contextPath; - private final WebContainer paxWeb; - private final List registeredServlets = new ArrayList<>(); - private final List registeredEventListeners = new ArrayList<>(); - private final List registeredFilters = new ArrayList<>(); - private final List registeredResources = new ArrayList<>(); - - WebContextImpl(final WebContainer paxWeb, final WebContext webContext) throws ServletException { - // We ignore webContext.supportsSessions() because the OSGi HttpService / Pax Web API - // does not seem to support not wanting session support on some web contexts - // (it assumes always with session); but other implementation support without. - - this.paxWeb = paxWeb; - contextPath = webContext.contextPath(); - - // NB This is NOT the URL prefix of the context, but the context.id which is - // used while registering the HttpContext in the OSGi service registry. - String contextID = contextPath + ".id"; - - HttpContext osgiHttpContext = paxWeb.createDefaultHttpContext(contextID); - paxWeb.begin(osgiHttpContext); - - // The order in which we set things up here matters... - - // 1. Context parameters - because listeners, filters and servlets could need them - paxWeb.setContextParam(new MapDictionary<>(webContext.contextParams()), osgiHttpContext); - - // 2. Listeners - because they could set up things that filters and servlets need - for (ServletContextListener listener : webContext.listeners()) { - registerListener(osgiHttpContext, listener); - } - - // 3. Filters - because subsequent servlets should already be covered by the filters - for (FilterDetails filter : webContext.filters()) { - registerFilter(osgiHttpContext, filter.urlPatterns(), filter.name(), filter.filter(), - filter.initParams(), filter.getAsyncSupported()); - } - - // 4. servlets - 'bout time for 'em by now, don't you think? ;) - for (ServletDetails servlet : webContext.servlets()) { - registerServlet(osgiHttpContext, servlet.urlPatterns(), servlet.name(), servlet.servlet(), - servlet.initParams(), servlet.getAsyncSupported()); - } - - try { - for (ResourceDetails resource: webContext.resources()) { - String alias = ensurePrependedSlash(contextPath + ensurePrependedSlash(resource.alias())); - paxWeb.registerResources(alias, ensurePrependedSlash(resource.name()), osgiHttpContext); - registeredResources.add(alias); - } - } catch (NamespaceException e) { - throw new ServletException("Error registering resources", e); - } - - paxWeb.end(osgiHttpContext); - } - - private static String ensurePrependedSlash(final String str) { - return str.startsWith("/") ? str : "/" + str; - } - - private void registerFilter(final HttpContext osgiHttpContext, final List urlPatterns, - final String name, final Filter filter, final Map params, - final Boolean asyncSupported) { - final String[] absUrlPatterns = absolute(urlPatterns); - LOG.info("Registering Filter for aliases {}: {} with async: {}", Arrays.asList(absUrlPatterns), - filter, asyncSupported); - paxWeb.registerFilter(filter, absUrlPatterns, new String[] { name }, new MapDictionary<>(params), - asyncSupported, osgiHttpContext); - registeredFilters.add(filter); - } - - private String[] absolute(final List relatives) { - return relatives.stream().map(urlPattern -> contextPath + urlPattern).toArray(String[]::new); - } - - private void registerServlet(final HttpContext osgiHttpContext, final List urlPatterns, - final String name, final Servlet servlet, final Map params, - final Boolean asyncSupported) throws ServletException { - int loadOnStartup = 1; - String[] absUrlPatterns = absolute(urlPatterns); - LOG.info("Registering Servlet for aliases {}: {} with async: {}", absUrlPatterns, - servlet, asyncSupported); - paxWeb.registerServlet(servlet, name, absUrlPatterns, new MapDictionary<>(params), loadOnStartup, - asyncSupported, osgiHttpContext); - registeredServlets.add(servlet); - } - - private void registerListener(final HttpContext osgiHttpContext, final ServletContextListener listener) { - paxWeb.registerEventListener(listener, osgiHttpContext); - registeredEventListeners.add(listener); - } - - @Override - protected void removeRegistration() { - // The order is relevant here.. Servlets first, then Filters, Listeners last; this is the inverse of above - registeredServlets.forEach(paxWeb::unregisterServlet); - registeredFilters.forEach(paxWeb::unregisterFilter); - registeredEventListeners.forEach(paxWeb::unregisterEventListener); - registeredResources.forEach(paxWeb::unregister); - } - } -} diff --git a/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardResource.java b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardResource.java new file mode 100644 index 000000000..5b43590a5 --- /dev/null +++ b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardResource.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.web.osgi; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * A dummy class to support HTTP whiteboard resource registration. + */ +final class WhiteboardResource { + static final @NonNull WhiteboardResource INSTANCE = new WhiteboardResource(); + + private WhiteboardResource() { + // Hidden on purpose + } +} diff --git a/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardServletContextHelper.java b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardServletContextHelper.java new file mode 100644 index 000000000..247362e5b --- /dev/null +++ b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardServletContextHelper.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.web.osgi; + +import org.osgi.framework.Bundle; +import org.osgi.service.http.context.ServletContextHelper; + +/** + * Custom {@link ServletContextHelper} for use as the top encapsulating object. + */ +final class WhiteboardServletContextHelper extends ServletContextHelper { + WhiteboardServletContextHelper(final Bundle bundle) { + super(bundle); + } +} diff --git a/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardWebServer.java b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardWebServer.java new file mode 100644 index 000000000..ad98ae039 --- /dev/null +++ b/web/impl-osgi/src/main/java/org/opendaylight/aaa/web/osgi/WhiteboardWebServer.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.web.osgi; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.Filter; +import javax.servlet.Servlet; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import org.opendaylight.aaa.web.FilterDetails; +import org.opendaylight.aaa.web.ResourceDetails; +import org.opendaylight.aaa.web.ServletDetails; +import org.opendaylight.aaa.web.WebContext; +import org.opendaylight.aaa.web.WebServer; +import org.opendaylight.yangtools.concepts.AbstractRegistration; +import org.opendaylight.yangtools.concepts.Registration; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; +import org.osgi.service.http.context.ServletContextHelper; +import org.osgi.service.http.runtime.HttpServiceRuntime; +import org.osgi.service.http.runtime.HttpServiceRuntimeConstants; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.service.http.whiteboard.annotations.RequireHttpWhiteboard; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link WebServer} implementation based on + * OSGi HTTP Whiteboard. + */ +@RequireHttpWhiteboard +@Component(scope = ServiceScope.BUNDLE) +public final class WhiteboardWebServer implements WebServer { + private static final Logger LOG = LoggerFactory.getLogger(WhiteboardWebServer.class); + + private final Bundle bundle; + @Reference + private volatile ServiceReference serviceRuntime; + + /** + * Construct a {@link WhiteboardWebServer} to a {@link ComponentContext}. + * + * @param componentContext A {@link ComponentContext} + */ + @Activate + public WhiteboardWebServer(final ComponentContext componentContext) { + bundle = componentContext.getUsingBundle(); + LOG.debug("Activated WebServer for bundle {}", bundle); + } + + @Deactivate + void deactivate() { + LOG.debug("Deactivated WebServer for bundle {}", bundle); + } + + @Override + public String getBaseURL() { + final var endpoint = serviceRuntime.getProperty(HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT); + if (endpoint instanceof String) { + return (String) endpoint; + } else if (endpoint instanceof String[]) { + return getBaseURL(Arrays.asList((String[]) endpoint)); + } else if (endpoint instanceof Collection) { + // Safe as per OSGi Compendium R7 section 140.15.3.1 + @SuppressWarnings("unchecked") + final var cast = (Collection) endpoint; + return getBaseURL(cast); + } else { + throw new IllegalStateException("Unhandled endpoint " + endpoint); + } + } + + private static String getBaseURL(final Collection endpoints) { + for (var endpoint : endpoints) { + if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) { + return endpoint; + } + } + throw new IllegalStateException("Cannot select base URL from " + endpoints); + } + + @Override + public Registration registerWebContext(final WebContext webContext) throws ServletException { + final var bundleContext = bundle.getBundleContext(); + final var builder = ImmutableList.>builder(); + + // The order in which we set things up here matters... + + // 1. ServletContextHelper, to which all others are bound to + final var contextPath = absolutePath(webContext.contextPath()); + // TODO: can we create a better name? + final var contextName = contextPath + ".id"; + + final var contextProps = contextProperties(contextName, contextPath, webContext.contextParams()); + LOG.debug("Registering context {} with properties {}", contextName, contextProps); + builder.add(bundleContext.registerService(ServletContextHelper.class, + new WhiteboardServletContextHelper(bundle), new MapDictionary<>(contextProps))); + + // 2. Listeners - because they could set up things that filters and servlets need + final var contextSelect = "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + contextName + ")"; + for (var listener : webContext.listeners()) { + final var props = Map.of( + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, contextSelect, + HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER, Boolean.TRUE); + LOG.debug("Registering listener {} with properties {}", listener, props); + builder.add(bundleContext.registerService(ServletContextListener.class, listener, + new MapDictionary<>(props))); + } + + // 3. Filters - because subsequent servlets should already be covered by the filters + for (var filter : webContext.filters()) { + final var props = filterProperties(contextPath, contextSelect, filter); + LOG.debug("Registering filter {} with properties {}", filter, props); + builder.add(bundleContext.registerService(Filter.class, filter.filter(), new MapDictionary<>(props))); + } + + // 4. Servlets - 'bout time for 'em by now, don't you think? ;) + for (var servlet : webContext.servlets()) { + final var props = servletProperties(contextPath, contextSelect, servlet); + LOG.debug("Registering servlet {} with properties {}", servlet, props); + builder.add(bundleContext.registerService(Servlet.class, servlet.servlet(), new MapDictionary<>(props))); + } + + // 5. Resources + for (var resource : webContext.resources()) { + final var props = resourceProperties(contextPath, contextSelect, resource); + LOG.debug("Registering resource {} with properties {}", resource, props); + builder.add(bundleContext.registerService(Object.class, WhiteboardResource.INSTANCE, + new MapDictionary<>(props))); + } + + final var services = builder.build(); + LOG.info("Bundle {} registered context path {} with {} service(s)", bundle, contextPath, services.size()); + return new AbstractRegistration() { + @Override + protected void removeRegistration() { + // The order does not have to be reversed: we unregister ServletContextHelper first, hence everybody + // becomes unbound + services.forEach(ServiceRegistration::unregister); + } + }; + } + + private static Map contextProperties(final String contextName, final String contextPath, + final Map params) { + final var builder = ImmutableMap.builder() + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME, contextName) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH, contextPath); + + for (var e : params.entrySet()) { + builder.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_INIT_PARAM_PREFIX + e.getKey(), e.getValue()); + } + + return builder.build(); + } + + private static Map filterProperties(final String contextPath, final String contextSelect, + final FilterDetails filter) { + final var builder = ImmutableMap.builder() + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, contextSelect) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_ASYNC_SUPPORTED, filter.getAsyncSupported()) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_NAME, filter.name()) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, + absolutePatterns(contextPath, filter.urlPatterns())); + + for (var e : filter.initParams().entrySet()) { + builder.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + e.getKey(), e.getValue()); + } + + return builder.build(); + } + + private static Map resourceProperties(final String contextPath, final String contextSelect, + final ResourceDetails resource) { + final var path = absolutePath(resource.name()); + return Map.of( + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, contextSelect, + HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PATTERN, contextPath + absolutePath(resource.alias()), + HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PREFIX, path); + } + + private static Map servletProperties(final String contextPath, final String contextSelect, + final ServletDetails servlet) { + final var builder = ImmutableMap.builder() + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, contextSelect) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED, servlet.getAsyncSupported()) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, servlet.name()) + .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, + absolutePatterns(contextPath, servlet.urlPatterns())); + + for (var e : servlet.initParams().entrySet()) { + builder.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + e.getKey(), e.getValue()); + } + + return builder.build(); + } + + private static String absolutePath(final String path) { + return path.startsWith("/") ? path : "/" + path; + } + + private static List absolutePatterns(final String contextPath, final List urlPatterns) { + return urlPatterns.stream() + // Reject duplicates + .distinct() + // Ease of debugging + .sorted() + .map(urlPattern -> contextPath + urlPattern) + .collect(Collectors.toUnmodifiableList()); + } +} -- 2.36.6