Document and validate web-api constructs
[aaa.git] / web / api / src / main / java / org / opendaylight / aaa / web / WebContext.java
index a8ca1a0963bf2d672b54de43b4d07dbd82c408b7..76928a02599e68502e794bbc0128b05f7980577b 100644 (file)
  */
 package org.opendaylight.aaa.web;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 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;
+import org.eclipse.jdt.annotation.NonNull;
 
 /**
  * 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.)
+ * Its {@link WebContext.Builder} 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.
+ *   <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>
+ *   <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} etc. that can be avoided
+ *       using this;</li>
+ *   <li>tests can more easily programmatically instantiate web components.</li>
  * </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.)
+ * 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.)
+ * 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)}.
+ * 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();
-    }
-
+public interface WebContext {
     /**
-     * Path which will be used as URL prefix to all registered servlets and filters.
+     * Get path which will be used as URL prefix to all registered servlets and filters. Guaranteed to be non-empty
+     *
+     * @return {@link String} path
+     * @see "Java Servlet Specification Version 3.1, Section 3.5 Request Path Elements"
      */
-    public abstract String contextPath();
+    @NonNull String contextPath();
 
     /**
-     * Flag whether this context supports web sessions, defaults to true.
+     * Get flag value whether this context supports web sessions.
+     *
+     * @return boolean flag value
      */
-    @Default
-    public boolean supportsSessions() {
-        return true;
-    }
+    boolean supportsSessions();
 
     /**
-     * Servlets.
+     * Get list of servlets.
+     *
+     * @return {@link List} list of {@link ServletDetails}
      */
-    public abstract List<ServletDetails> servlets();
+    @NonNull List<ServletDetails> servlets();
 
     /**
-     * Filters.
+     * Get list of filters.
+     *
+     * @return {@link List} list of {@link FilterDetails}
      */
-    public abstract List<FilterDetails> filters();
+    @NonNull List<FilterDetails> filters();
 
     /**
-     * Listeners.
+     * Get list of servlet context listeners.
+     *
+     * @return {@link List} list of {@link ServletContextListener}
      */
-    public abstract List<ServletContextListener> listeners();
+    @NonNull List<ServletContextListener> listeners();
 
     /**
-     * Registers resources (eg html files) that can be accessed via the URI namespace.
+     * Get lis of resources (e.g. html files) that can be accessed via the URI namespace.
+     *
+     * @return {@link List} list of {@link ResourceDetails}
      */
-    public abstract List<ResourceDetails> resources();
+    @NonNull List<ResourceDetails> resources();
 
     /**
-     * Context params. These are the {@link ServletContext}s initial parameters; contrary to individual
+     * Get map of context params.
+     *
+     * <p>
+     * These are the {@link ServletContext}s initial parameters; contrary to individual
      * {@link ServletDetails#initParams()} and {@link FilterDetails#initParams()}. While a ServletContext accepts
      * any Object as a parameter, that is not accepted in all implementations. Most notably OSGi HTTP Whiteboard
      * specification allows only String values, hence we are enforcing that.
+     *
+     * @return {@link Map} context parameters map
      */
-    public abstract Map<String, String> contextParams();
+    @NonNull 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());
+    /**
+     * Create builder for {@code WebContext}.
+     *
+     * @return {@link Builder} builder instance
+     */
+    static @NonNull Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builds instances of type {@link WebContext WebContext}. Initialize attributes and then invoke the
+     * {@link #build()} method to create an immutable instance.
+     *
+     * <p><em>{@code WebContext.Builder} is not thread-safe and generally should not be stored in a field or
+     * collection, but instead used immediately to create instances.</em>
+     */
+    final class Builder {
+        private record ImmutableWebContext(String contextPath, ImmutableList<ServletDetails> servlets,
+            ImmutableList<FilterDetails> filters, ImmutableList<ServletContextListener> listeners,
+            ImmutableList<ResourceDetails> resources, ImmutableMap<String, String> contextParams,
+            boolean supportsSessions) implements WebContext {
+            // Not much else here
+        }
+
+        private final ImmutableMap.Builder<String, String> contextParams = ImmutableMap.builder();
+        private final ImmutableList.Builder<ServletDetails> servlets = ImmutableList.builder();
+        private final ImmutableList.Builder<FilterDetails> filters = ImmutableList.builder();
+        private final ImmutableList.Builder<ServletContextListener> listeners = ImmutableList.builder();
+        private final ImmutableList.Builder<ResourceDetails> resources = ImmutableList.builder();
+        private String contextPath;
+        private boolean supportsSessions = true;
+
+        private Builder() {
+            // Hidden on purpose
+        }
+
+        /**
+         * Initializes the value for the {@link WebContext#contextPath() contextPath} attribute. As per Servlet
+         *
+         * @param contextPath The value for contextPath
+         * @return {@code this} builder for use in a chained invocation
+         * @throws IllegalArgumentException if {@code contextPath} does not meet specification criteria
+         * @throws NullPointerException if {code contextPath} is {@code null}
+         */
+        @SuppressWarnings("checkstyle:hiddenField")
+        public @NonNull Builder contextPath(final String contextPath) {
+            this.contextPath = ServletSpec.requireContextPath(contextPath);
+            return this;
+        }
+
+        /**
+         * Adds one element to {@link WebContext#servlets() servlets} list.
+         *
+         * @param servlet A servlets element
+         * @return {@code this} builder for use in a chained invocation
+         * @throws NullPointerException if {code servlet} is {@code null}
+         */
+        public @NonNull Builder addServlet(final ServletDetails servlet) {
+            servlets.add(servlet);
+            return this;
+        }
+
+        /**
+         * Adds one element to {@link WebContext#filters() filters} list.
+         *
+         * @param filter A filters element
+         * @return {@code this} builder for use in a chained invocation
+         * @throws NullPointerException if {code filter} is {@code null}
+         */
+        public @NonNull Builder addFilter(final FilterDetails filter) {
+            filters.add(filter);
+            return this;
+        }
+
+        /**
+         * Adds one element to {@link WebContext#listeners() listeners} list.
+         *
+         * @param listener A listeners element
+         * @return {@code this} builder for use in a chained invocation
+         * @throws NullPointerException if {code listener} is {@code null}
+         */
+        public @NonNull Builder addListener(final ServletContextListener listener) {
+            listeners.add(listener);
+            return this;
+        }
+
+        /**
+         * Adds one element to {@link WebContext#resources() resources} list.
+         *
+         * @param resource A resources element
+         * @return {@code this} builder for use in a chained invocation
+         * @throws NullPointerException if {code resource} is {@code null}
+         */
+        public @NonNull Builder addResource(final ResourceDetails resource) {
+            resources.add(resource);
+            return this;
+        }
+
+        /**
+         * Put one entry to the {@link WebContext#contextParams() contextParams} map.
+         *
+         * @param key The key in the contextParams map
+         * @param value The associated value in the contextParams map
+         * @return {@code this} builder for use in a chained invocation
+         * @throws NullPointerException if any argument is {@code null}
+         */
+        public @NonNull Builder putContextParam(final String key, final String value) {
+            contextParams.put(key, value);
+            return this;
+        }
+
+        /**
+         * Initializes the value for the {@link WebContext#supportsSessions() supportsSessions} attribute.
+         *
+         * <p><em>If not set, this attribute will have a default value of {@code true}.</em>
+         *
+         * @param supportsSessions The value for supportsSessions
+         * @return {@code this} builder for use in a chained invocation
+         */
+        @SuppressWarnings("checkstyle:hiddenField")
+        public Builder supportsSessions(final boolean supportsSessions) {
+            this.supportsSessions = supportsSessions;
+            return this;
+        }
+
+        /**
+         * Builds a new {@link WebContext WebContext}.
+         *
+         * @return An immutable instance of WebContext
+         * @throws IllegalStateException if any required attributes are missing
+         */
+        public @NonNull WebContext build() {
+            if (contextPath == null) {
+                throw new IllegalStateException("No contextPath specified");
             }
-        });
+            return new ImmutableWebContext(contextPath, servlets.build(), filters.build(), listeners.build(),
+                resources.build(), contextParams.build(), supportsSessions);
+        }
     }
 }