Add support for descriptive WebContext name
[aaa.git] / web / api / src / main / java / org / opendaylight / aaa / web / WebContext.java
1 /*
2  * Copyright (c) 2018 Red Hat, Inc. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.aaa.web;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.ImmutableList;
13 import com.google.common.collect.ImmutableMap;
14 import java.util.List;
15 import java.util.Map;
16 import javax.servlet.ServletContainerInitializer;
17 import javax.servlet.ServletContext;
18 import javax.servlet.ServletContextListener;
19 import javax.servlet.ServletRegistration;
20 import org.eclipse.jdt.annotation.NonNull;
21
22 /**
23  * Web Context with URL prefix. AKA Web App or Servlet context.
24  *
25  * <p>
26  * Its {@link WebContext.Builder} allows programmatic web component registration (as opposed to declarative e.g. via
27  * web.xml, OSGi HTTP Whiteboard blueprint integration, CXF BP etc.)
28  *
29  * <p>
30  * This is preferable because:
31  * <ul>
32  *   <li>using code instead of hiding class names in XML enables tools such as e.g. BND (in the maven-bundle-plugin) to
33  *       correctly figure dependencies e.g. for OSGi Import-Package headers;</li>
34  *   <li>explicit passing of web components instances, instead of providing class names in XML files and letting a web
35  *       container create the new instances using the default constructor, solves a pesky dependency injection (DI)
36  *       related problem which typically leads to weird hoops in code through {@code static} etc. that can be avoided
37  *       using this;</li>
38  *   <li>tests can more easily programmatically instantiate web components.</li>
39  * </ul>
40  *
41  * <p>
42  * This, not surprisingly, looks somewhat like a Servlet (3.x+) {@link ServletContext}, which also allows programmatic
43  * dynamic registration e.g. via {@link ServletRegistration}; however in practice direct use of that API has been found
44  * to be problematic under OSGi, because it is intended for JSE and
45  * <a href="https://github.com/eclipse/jetty.project/issues/1395">does not easily appear to permit dynamic registration
46  * at any time</a> (only during Servlet container initialization time by {@link ServletContainerInitializer}), and is
47  * generally less clear to use than this simple API which intentionally maps directly to what one would have declared in
48  * a web.xml file. This API is also slightly more focused and drops a number of concepts that API has which we do not
49  * want to support here (including e.g. security, roles, multipart etc.)
50  *
51  * <p>
52  * It also looks somewhat similar to the OSGi HttpService, but we want to avoid any org.osgi dependency (both API and
53  * impl) here, and that API is also less clear (and uses an ancient (!) {@link java.util.Dictionary} in its method
54  * signature), and -most importantly- simply does not support Filters and Listeners, only Servlets. The Pax Web API does
55  * extend the base OSGi API and adds supports for Filters, Listeners and context parameters, but is still OSGi specific,
56  * whereas this offers a much simpler standalone API without OSGi dependency. (The Pax Web API also has confusing
57  * signatures in its registerFilter() methods, where one can easily confuse which String[] is the urlPatterns; which we
58  * had initially done accidentally; and left AAA broken.)
59  *
60  * <p>
61  * This is immutable, with a Builder, because contrary to a declarative approach in a file such as web.xml, the
62  * registration order very much matters (e.g. an context parameter added after a Servlet registration would not be seen
63  * by that Servlet; or a Filter added to protect a Servlet might not yet be active for an instant if the registerServlet
64  * is before the registerFilter). Therefore, this API enforces atomicity and lets clients first register everything on
65  * the Builder, and only then use {@link WebServer#registerWebContext(WebContext)}.
66  *
67  * @author Michael Vorburger.ch
68  */
69 public interface WebContext {
70     /**
71      * Get the descriptive name of this context.
72      *
73      * @return A descriptive name.
74      */
75     @NonNull String name();
76
77     /**
78      * Get path which will be used as URL prefix to all registered servlets and filters. Guaranteed to be non-empty
79      *
80      * @return {@link String} path
81      * @see "Java Servlet Specification Version 3.1, Section 3.5 Request Path Elements"
82      */
83     @NonNull String contextPath();
84
85     /**
86      * Get flag value whether this context supports web sessions.
87      *
88      * @return boolean flag value
89      */
90     boolean supportsSessions();
91
92     /**
93      * Get list of servlets.
94      *
95      * @return {@link List} list of {@link ServletDetails}
96      */
97     @NonNull List<ServletDetails> servlets();
98
99     /**
100      * Get list of filters.
101      *
102      * @return {@link List} list of {@link FilterDetails}
103      */
104     @NonNull List<FilterDetails> filters();
105
106     /**
107      * Get list of servlet context listeners.
108      *
109      * @return {@link List} list of {@link ServletContextListener}
110      */
111     @NonNull List<ServletContextListener> listeners();
112
113     /**
114      * Get lis of resources (e.g. html files) that can be accessed via the URI namespace.
115      *
116      * @return {@link List} list of {@link ResourceDetails}
117      */
118     @NonNull List<ResourceDetails> resources();
119
120     /**
121      * Get map of context params.
122      *
123      * <p>
124      * These are the {@link ServletContext}s initial parameters; contrary to individual
125      * {@link ServletDetails#initParams()} and {@link FilterDetails#initParams()}. While a ServletContext accepts
126      * any Object as a parameter, that is not accepted in all implementations. Most notably OSGi HTTP Whiteboard
127      * specification allows only String values, hence we are enforcing that.
128      *
129      * @return {@link Map} context parameters map
130      */
131     @NonNull Map<String, String> contextParams();
132
133     /**
134      * Create builder for {@code WebContext}.
135      *
136      * @return {@link Builder} builder instance
137      */
138     static @NonNull Builder builder() {
139         return new Builder();
140     }
141
142     /**
143      * Builds instances of type {@link WebContext WebContext}. Initialize attributes and then invoke the
144      * {@link #build()} method to create an immutable instance.
145      *
146      * <p><em>{@code WebContext.Builder} is not thread-safe and generally should not be stored in a field or
147      * collection, but instead used immediately to create instances.</em>
148      */
149     final class Builder {
150         private record ImmutableWebContext(String name, String contextPath, ImmutableList<ServletDetails> servlets,
151             ImmutableList<FilterDetails> filters, ImmutableList<ServletContextListener> listeners,
152             ImmutableList<ResourceDetails> resources, ImmutableMap<String, String> contextParams,
153             boolean supportsSessions) implements WebContext {
154             // Not much else here
155         }
156
157         private final ImmutableMap.Builder<String, String> contextParams = ImmutableMap.builder();
158         private final ImmutableList.Builder<ServletDetails> servlets = ImmutableList.builder();
159         private final ImmutableList.Builder<FilterDetails> filters = ImmutableList.builder();
160         private final ImmutableList.Builder<ServletContextListener> listeners = ImmutableList.builder();
161         private final ImmutableList.Builder<ResourceDetails> resources = ImmutableList.builder();
162         private String name;
163         private String contextPath;
164         private boolean supportsSessions = true;
165
166         private Builder() {
167             // Hidden on purpose
168         }
169
170         /**
171          * Initializes the value for the {@link WebContext#name() name} attribute.
172          *
173          * @param name A descriptive name
174          * @return {@code this} builder for use in a chained invocation
175          * @throws IllegalArgumentException if {@code contextPath} does not meet specification criteria
176          * @throws NullPointerException if {code contextPath} is {@code null}
177          */
178         @SuppressWarnings("checkstyle:hiddenField")
179         public @NonNull Builder name(final String name) {
180             this.name = requireNonNull(name);
181             return this;
182         }
183
184         /**
185          * Initializes the value for the {@link WebContext#contextPath() contextPath} attribute. As per Servlet
186          * specification.
187          *
188          * @param contextPath The value for contextPath
189          * @return {@code this} builder for use in a chained invocation
190          * @throws IllegalArgumentException if {@code contextPath} does not meet specification criteria
191          * @throws NullPointerException if {code contextPath} is {@code null}
192          */
193         @SuppressWarnings("checkstyle:hiddenField")
194         public @NonNull Builder contextPath(final String contextPath) {
195             this.contextPath = ServletSpec.requireContextPath(contextPath);
196             return this;
197         }
198
199         /**
200          * Adds one element to {@link WebContext#servlets() servlets} list.
201          *
202          * @param servlet A servlets element
203          * @return {@code this} builder for use in a chained invocation
204          * @throws NullPointerException if {code servlet} is {@code null}
205          */
206         public @NonNull Builder addServlet(final ServletDetails servlet) {
207             servlets.add(servlet);
208             return this;
209         }
210
211         /**
212          * Adds one element to {@link WebContext#filters() filters} list.
213          *
214          * @param filter A filters element
215          * @return {@code this} builder for use in a chained invocation
216          * @throws NullPointerException if {code filter} is {@code null}
217          */
218         public @NonNull Builder addFilter(final FilterDetails filter) {
219             filters.add(filter);
220             return this;
221         }
222
223         /**
224          * Adds one element to {@link WebContext#listeners() listeners} list.
225          *
226          * @param listener A listeners element
227          * @return {@code this} builder for use in a chained invocation
228          * @throws NullPointerException if {code listener} is {@code null}
229          */
230         public @NonNull Builder addListener(final ServletContextListener listener) {
231             listeners.add(listener);
232             return this;
233         }
234
235         /**
236          * Adds one element to {@link WebContext#resources() resources} list.
237          *
238          * @param resource A resources element
239          * @return {@code this} builder for use in a chained invocation
240          * @throws NullPointerException if {code resource} is {@code null}
241          */
242         public @NonNull Builder addResource(final ResourceDetails resource) {
243             resources.add(resource);
244             return this;
245         }
246
247         /**
248          * Put one entry to the {@link WebContext#contextParams() contextParams} map.
249          *
250          * @param key The key in the contextParams map
251          * @param value The associated value in the contextParams map
252          * @return {@code this} builder for use in a chained invocation
253          * @throws NullPointerException if any argument is {@code null}
254          */
255         public @NonNull Builder putContextParam(final String key, final String value) {
256             contextParams.put(key, value);
257             return this;
258         }
259
260         /**
261          * Initializes the value for the {@link WebContext#supportsSessions() supportsSessions} attribute.
262          *
263          * <p><em>If not set, this attribute will have a default value of {@code true}.</em>
264          *
265          * @param supportsSessions The value for supportsSessions
266          * @return {@code this} builder for use in a chained invocation
267          */
268         @SuppressWarnings("checkstyle:hiddenField")
269         public Builder supportsSessions(final boolean supportsSessions) {
270             this.supportsSessions = supportsSessions;
271             return this;
272         }
273
274         /**
275          * Builds a new {@link WebContext WebContext}.
276          *
277          * @return An immutable instance of WebContext
278          * @throws IllegalStateException if any required attributes are missing
279          */
280         public @NonNull WebContext build() {
281             if (contextPath == null) {
282                 throw new IllegalStateException("No contextPath specified");
283             }
284
285             return new ImmutableWebContext(name != null ? name : contextPath + ".id", contextPath, servlets.build(),
286                 filters.build(), listeners.build(), resources.build(), contextParams.build(), supportsSessions);
287         }
288     }
289 }