add new API for programmatic registration of web Servlet, Filter, etc. 33/68833/12
authorMichael Vorburger <vorburger@redhat.com>
Tue, 27 Feb 2018 18:43:56 +0000 (19:43 +0100)
committerMichael Vorburger <vorburger@redhat.com>
Fri, 9 Mar 2018 18:49:00 +0000 (19:49 +0100)
implementation & usage of this can be seen in the next "chained" commits

The purpose of this API is to let projects with web components, such as
neutron, aaa or restconf, ditch their respective web.xml.  This will have
a number of advantages, some of which are documented in the JavaDoc of
the new WebServer interface and WebContext class.

see also discussion and interest from project neutron re. adoption on:
https://lists.opendaylight.org/pipermail/neutron-dev/2018-February/001587.html

This is the change originally raised in infrautils as
Ib2df87ca31a2bde547efbf73e0475a1cd64ea6ea, but now instead proposed
to aaa, as discussed during the Kernel Projects call on 2018/02/27.

Change-Id: Ib2fb02aa19e49aa482062f18ba84124a9a623364
Signed-off-by: Michael Vorburger <vorburger@redhat.com>
pom.xml
web/api/pom.xml [new file with mode: 0644]
web/api/src/main/java/org/opendaylight/aaa/web/FilterDetails.java [new file with mode: 0644]
web/api/src/main/java/org/opendaylight/aaa/web/ServletDetails.java [new file with mode: 0644]
web/api/src/main/java/org/opendaylight/aaa/web/WebContext.java [new file with mode: 0644]
web/api/src/main/java/org/opendaylight/aaa/web/WebContextRegistration.java [new file with mode: 0644]
web/api/src/main/java/org/opendaylight/aaa/web/WebServer.java [new file with mode: 0644]
web/api/src/test/java/org/opendaylight/aaa/web/tests/WebContextApiTest.java [new file with mode: 0644]
web/pom.xml [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index af803e9ec47584f49f7d9c414c5e8ad7bca958ca..3e43080b075de0fe953230c196c1d7ba7851183b 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,7 @@
     <module>aaa-shiro</module>
     <module>aaa-shiro-act</module>
     <module>dependency-check</module>
+    <module>web</module>
   </modules>
 
   <build>
diff --git a/web/api/pom.xml b/web/api/pom.xml
new file mode 100644 (file)
index 0000000..4b660eb
--- /dev/null
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright © 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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.odlparent</groupId>
+    <artifactId>bundle-parent</artifactId>
+    <version>3.0.2</version>
+    <relativePath />
+  </parent>
+
+  <groupId>org.opendaylight.aaa.web</groupId>
+  <artifactId>web-api</artifactId>
+  <version>0.8.0-SNAPSHOT</version>
+  <name>ODL :: infrautils :: ${project.artifactId}</name>
+  <packaging>bundle</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.immutables</groupId>
+      <artifactId>value</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.infrautils</groupId>
+      <artifactId>infrautils-testutils</artifactId>
+      <version>1.4.0-SNAPSHOT</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.aries.blueprint</groupId>
+        <artifactId>blueprint-maven-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <configuration>
+          <propertyExpansion>checkstyle.violationSeverity=error</propertyExpansion>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>findbugs-maven-plugin</artifactId>
+        <configuration>
+          <failOnError>true</failOnError>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/web/api/src/main/java/org/opendaylight/aaa/web/FilterDetails.java b/web/api/src/main/java/org/opendaylight/aaa/web/FilterDetails.java
new file mode 100644 (file)
index 0000000..b67d45a
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+import javax.servlet.Filter;
+import org.immutables.value.Value;
+import org.immutables.value.Value.Default;
+
+/**
+ * Details about a {@link Filter}.
+ *
+ * @author Michael Vorburger.ch
+ */
+@Value.Immutable
+@Value.Style(visibility = Value.Style.ImplementationVisibility.PRIVATE, depluralize = true)
+public interface FilterDetails {
+
+    static FilterDetailsBuilder builder() {
+        return new FilterDetailsBuilder();
+    }
+
+    Filter filter();
+
+    @Default default String name() {
+        return filter().getClass().getName();
+    }
+
+    List<String> urlPatterns();
+
+    Map<String, String> initParams();
+
+}
diff --git a/web/api/src/main/java/org/opendaylight/aaa/web/ServletDetails.java b/web/api/src/main/java/org/opendaylight/aaa/web/ServletDetails.java
new file mode 100644 (file)
index 0000000..2e90606
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+import javax.servlet.Servlet;
+import org.immutables.value.Value;
+import org.immutables.value.Value.Default;
+
+/**
+ * Details about a {@link Servlet}.
+ *
+ * @author Michael Vorburger.ch
+ */
+@Value.Immutable
+@Value.Style(visibility = Value.Style.ImplementationVisibility.PRIVATE, depluralize = true)
+public interface ServletDetails {
+
+    static ServletDetailsBuilder builder() {
+        return new ServletDetailsBuilder();
+    }
+
+    Servlet servlet();
+
+    @Default default String name() {
+        return servlet().getClass().getName();
+    }
+
+    List<String> urlPatterns();
+
+    Map<String, String> initParams();
+
+}
diff --git a/web/api/src/main/java/org/opendaylight/aaa/web/WebContext.java b/web/api/src/main/java/org/opendaylight/aaa/web/WebContext.java
new file mode 100644 (file)
index 0000000..f98393b
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+import javax.servlet.ServletContainerInitializer;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletRegistration;
+import org.immutables.value.Value;
+import org.immutables.value.Value.Default;
+
+/**
+ * Web Context with URL prefix. AKA Web App or Servlet context.
+ *
+ * <p>
+ * Its {@link WebContextBuilder} allows programmatic web component registration
+ * (as opposed to declarative e.g. via web.xml, OSGi HTTP Whiteboard blueprint
+ * integration, CXF BP etc.)
+ *
+ * <p>
+ * This is preferable because:
+ * <ul>
+ * <li>using code instead of hiding class names in XML enables tools such as
+ * e.g. BND (in the maven-bundle-plugin) to correctly figure dependencies e.g.
+ * for OSGi Import-Package headers;
+ *
+ * <li>explicit passing of web components instances, instead of providing class
+ * names in XML files and letting a web container create the new instances using
+ * the default constructor, solves a pesky dependency injection (DI) related
+ * problem which typically leads to weird hoops in code through
+ * <code>static</code> etc. that can be avoided using this;
+ *
+ * <li>tests can more easily programmatically instantiate web components.
+ * </ul>
+ *
+ * <p>
+ * This, not surprisingly, looks somewhat like a Servlet (3.x)
+ * {@link ServletContext}, which also allows programmatic dynamic registration
+ * e.g. via {@link ServletRegistration}; however in practice direct use of that
+ * API has been found to be problematic under OSGi, because it is intended for
+ * JSE and <a href="https://github.com/eclipse/jetty.project/issues/1395">does
+ * not easily appear to permit dynamic registration at any time</a> (only during
+ * Servlet container initialization time by
+ * {@link ServletContainerInitializer}), and is generally less clear to use than
+ * this simple API which intentionally maps directly to what one would have
+ * declared in a web.xml file. This API is also slightly more focused and drops
+ * a number of concepts that API has which we do not want to support here
+ * (including e.g. security, roles, multipart etc.)
+ *
+ * <p>
+ * It also looks somewhat similar to the OSGi HttpService, but we want to avoid
+ * any org.osgi dependency (both API and impl) here, and that API is also less
+ * clear (and uses an ancient (!) {@link java.util.Dictionary} in its method
+ * signature), and -most importantly- simply does not support Filters and Listeners, only
+ * Servlets. The Pax Web API does extend the base OSGi API and adds supports for
+ * Filters, Listeners and context parameters, but is still OSGi specific,
+ * whereas this offers a much simpler standalone API without OSGi dependency.
+ * (The Pax Web API also has confusing signatures in its registerFilter() methods,
+ * where one can easily confuse which String[] is the urlPatterns;
+ * which we had initially done accidentally; and left AAA broken.)
+ *
+ * <p>
+ * This is immutable, with a Builder, because contrary to a declarative approach
+ * in a file such as web.xml, the registration order very much matters (e.g. an
+ * context parameter added after a Servlet registration would not be seen by that
+ * Servlet; or a Filter added to protect a Servlet might not yet be active
+ * for an instant if the registerServlet is before the registerFilter).
+ * Therefore, this API enforces atomicity and lets clients first register
+ * everything on the Builder, and only then use
+ * {@link WebServer#registerWebContext(WebContext)}.
+ *
+ * @author Michael Vorburger.ch
+ */
+@Value.Immutable
+@Value.Style(visibility = Value.Style.ImplementationVisibility.PRIVATE, depluralize = true)
+public abstract class WebContext {
+
+    public static WebContextBuilder builder() {
+        return new WebContextBuilder();
+    }
+
+    /**
+     * Path which will be used as URL prefix to all registered servlets and filters.
+     */
+    public abstract String contextPath();
+
+    /**
+     * Flag whether this context supports web sessions, defaults to true.
+     */
+    @Default
+    public boolean supportsSessions() {
+        return true;
+    }
+
+    /**
+     * Servlets.
+     */
+    public abstract List<ServletDetails> servlets();
+
+    /**
+     * Filters.
+     */
+    public abstract List<FilterDetails> filters();
+
+    /**
+     * Listeners.
+     */
+    public abstract List<ServletContextListener> listeners();
+
+    /**
+     * Context params. NB: These are the web context's wide parameters; contrary to
+     * individual {@link ServletDetails#initParams()} and
+     * {@link FilterDetails#initParams()}.
+     */
+    public abstract Map<String, String> contextParams();
+
+    @Value.Check
+    protected void check() {
+        servlets().forEach(servlet -> {
+            if (servlet.urlPatterns().isEmpty()) {
+                throw new IllegalArgumentException("Servlet has no URL: " + servlet.name());
+            }
+        });
+        filters().forEach(filter -> {
+            if (filter.urlPatterns().isEmpty()) {
+                throw new IllegalArgumentException("Filter has no URL: " + filter.name());
+            }
+        });
+    }
+}
diff --git a/web/api/src/main/java/org/opendaylight/aaa/web/WebContextRegistration.java b/web/api/src/main/java/org/opendaylight/aaa/web/WebContextRegistration.java
new file mode 100644 (file)
index 0000000..9faab1e
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+/**
+ * {@link WebContext} registration.
+ * Allows to {@link #close()} the web context, which unregisters its servlets, filters and listeners.
+ *
+ * @author Michael Vorburger.ch
+ */
+public interface WebContextRegistration extends AutoCloseable {
+
+    @Override
+    void close(); // does not throw Exception
+
+}
diff --git a/web/api/src/main/java/org/opendaylight/aaa/web/WebServer.java b/web/api/src/main/java/org/opendaylight/aaa/web/WebServer.java
new file mode 100644 (file)
index 0000000..ceb24f3
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import javax.servlet.ServletException;
+
+/**
+ * Web server (HTTP). This service API allows ODL applications to register web
+ * components programmatically, instead of using a web.xml declaratively; see
+ * the {@link WebContext} for why this is preferable.
+ *
+ * <p>
+ * This API has an OSGi-based as well as a "standalone" implementation suitable
+ * e.g. for tests.
+ *
+ * @author Michael Vorburger.ch
+ */
+public interface WebServer {
+
+    /**
+     * Register a new web context.
+     *
+     * @param webContext the web context
+     * @return registration which allows to close the context (and remove its servlets etc.)
+     * @throws ServletException if registration of any of the components of the web context failed
+     */
+    WebContextRegistration registerWebContext(WebContext webContext) throws ServletException;
+
+    /**
+     * Base URL of this web server, without any contexts. In production, this would
+     * likely be HTTPS with a well known hostname and fixed port configured e.g. in
+     * a Karaf etc/ configuration file. In tests, this would be typically be HTTP on
+     * localhost and an arbitrarily chosen port.
+     *
+     * @return base URL, with http[s] prefix and port, NOT ending in slash
+     */
+    String getBaseURL();
+
+}
diff --git a/web/api/src/test/java/org/opendaylight/aaa/web/tests/WebContextApiTest.java b/web/api/src/test/java/org/opendaylight/aaa/web/tests/WebContextApiTest.java
new file mode 100644 (file)
index 0000000..7879c2e
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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.tests;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.opendaylight.infrautils.testutils.Asserts.assertThrows;
+
+import javax.servlet.Filter;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContextListener;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.opendaylight.aaa.web.FilterDetails;
+import org.opendaylight.aaa.web.ServletDetails;
+import org.opendaylight.aaa.web.WebContext;
+import org.opendaylight.aaa.web.WebServer;
+
+/**
+ * Tests for Web Server {@link WebContext} API. These tests don't test an
+ * {@link WebServer} implementation; the purpose is just to compile time check
+ * against the API signatures, notably incl. their generated code.
+ *
+ * @author Michael Vorburger.ch
+ */
+public class WebContextApiTest {
+
+    @Test
+    public void testEmptyBuilder() {
+        assertThrows(IllegalStateException.class, () -> WebContext.builder().build());
+    }
+
+    @Test
+    public void testMinimalBuilder() {
+        assertThat(WebContext.builder().contextPath("test").build().supportsSessions()).isTrue();
+        assertThat(WebContext.builder().contextPath("test").supportsSessions(false).build().contextPath())
+                .isEqualTo("test");
+    }
+
+    @Test
+    public void testAddSimpleServlet() {
+        WebContext webContext = WebContext.builder().contextPath("test")
+                .addServlet(ServletDetails.builder().servlet(mock(Servlet.class)).addUrlPattern("test").build())
+                .build();
+        assertThat(webContext.servlets()).hasSize(1);
+        ServletDetails firstServletDetail = webContext.servlets().get(0);
+        assertThat(firstServletDetail.name())
+                .startsWith("$javax.servlet.Servlet$$EnhancerByMockitoWithCGLIB$$");
+        assertThat(firstServletDetail.initParams()).isEmpty();
+    }
+
+    @Test
+    public void testAddFullServlet() {
+        WebContext.builder().contextPath("test").addServlet(ServletDetails.builder().servlet(mock(Servlet.class))
+                .addUrlPattern("test").addUrlPattern("another").name("custom").putInitParam("key", "value").build())
+                .build();
+    }
+
+    @Test
+    public void testAddFilter() {
+        WebContext.builder().contextPath("test")
+            .addFilter(FilterDetails.builder().filter(mock(Filter.class)).addUrlPattern("test").build()).build();
+    }
+
+    @Test
+    public void testAddListener() {
+        assertThat(WebContext.builder().contextPath("test").addListener(mock(ServletContextListener.class)).build()
+                .listeners()).isNotEmpty();
+    }
+
+    @Test
+    public void testContextParam() {
+        assertThat(WebContext.builder().contextPath("test").putContextParam("key", "value").build().contextParams())
+                .containsExactly("key", "value").inOrder();
+    }
+
+    @Test
+    @Ignore // TODO
+    public void testBadContextPath() {
+        assertThrows(IllegalArgumentException.class, () -> WebContext.builder().contextPath("test/sub").build());
+        assertThrows(IllegalArgumentException.class, () -> WebContext.builder().contextPath("test space").build());
+        assertThrows(IllegalArgumentException.class, () -> WebContext.builder().contextPath("/test").build());
+        assertThrows(IllegalArgumentException.class, () -> WebContext.builder().contextPath("test/").build());
+    }
+
+    @Test
+    @Ignore // TODO
+    public void testBadServletWithoutAnyURL() {
+        assertThrows(IllegalArgumentException.class, () -> WebContext.builder().contextPath("test")
+                .addServlet(ServletDetails.builder().servlet(mock(Servlet.class)).build()).build());
+    }
+
+    @Test
+    @Ignore // TODO
+    public void testBadFilterWithoutAnyURL() {
+        assertThrows(IllegalArgumentException.class, () -> WebContext.builder().contextPath("test")
+                .addFilter(FilterDetails.builder().filter(mock(Filter.class)).build()).build());
+    }
+
+}
diff --git a/web/pom.xml b/web/pom.xml
new file mode 100644 (file)
index 0000000..f3a4334
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- vi: set et smarttab sw=2 tabstop=2: -->
+<!--
+ Copyright © 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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.odlparent</groupId>
+    <artifactId>odlparent-lite</artifactId>
+    <version>3.0.2</version>
+    <relativePath />
+  </parent>
+
+  <groupId>org.opendaylight.aaa.web</groupId>
+  <artifactId>web-aggregator</artifactId>
+  <version>0.8.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+  <name>ODL :: infrautils :: ${project.artifactId}</name>
+
+  <modules>
+    <module>api</module>
+  </modules>
+</project>