import static java.util.Objects.requireNonNull;
+import java.util.Arrays;
import org.apache.shiro.web.env.WebEnvironment;
import org.opendaylight.aaa.shiro.filters.AAAShiroFilter;
import org.opendaylight.aaa.web.FilterDetails;
import org.opendaylight.aaa.web.WebContext;
-import org.opendaylight.aaa.web.WebContextBuilder;
import org.opendaylight.aaa.web.WebContextSecurer;
/**
}
@Override
- public void requireAuthentication(final WebContextBuilder webContextBuilder, final boolean asyncSupported,
+ public void requireAuthentication(final WebContext.Builder webContextBuilder, final boolean asyncSupported,
final String... urlPatterns) {
- webContextBuilder
- // AAA filter in front of these REST web services as well as for moon endpoints
- .addFilter(FilterDetails.builder()
- .filter(new AAAShiroFilter(webEnvironment))
- .addUrlPatterns(urlPatterns)
- .asyncSupported(asyncSupported)
- .build());
+ // AAA filter in front of these REST web services as well as for moon endpoints
+ final var filterBuilder = FilterDetails.builder()
+ .filter(new AAAShiroFilter(webEnvironment))
+ .asyncSupported(asyncSupported);
+ Arrays.stream(urlPatterns).forEach(filterBuilder::addUrlPattern);
+
+ webContextBuilder.addFilter(filterBuilder.build());
}
}
import org.opendaylight.aaa.web.FilterDetails;
import org.opendaylight.aaa.web.ServletDetails;
import org.opendaylight.aaa.web.WebContext;
-import org.opendaylight.aaa.web.WebContextBuilder;
import org.opendaylight.aaa.web.WebContextSecurer;
import org.opendaylight.aaa.web.WebServer;
import org.opendaylight.aaa.web.servlet.ServletSupport;
final WebContextSecurer webContextSecurer, final ServletSupport servletSupport,
final CustomFilterAdapterConfiguration customFilterAdapterConfig) throws ServletException {
- WebContextBuilder webContextBuilder = WebContext.builder().contextPath("auth").supportsSessions(true)
+ final var webContextBuilder = WebContext.builder()
+ .contextPath("/auth")
+ .supportsSessions(true)
- .addServlet(ServletDetails.builder().servlet(servletSupport.createHttpServletBuilder(
- new IdmLightApplication(iidMStore, claimCache)).build())
- .addUrlPattern("/*").build())
+ .addServlet(ServletDetails.builder()
+ .servlet(servletSupport.createHttpServletBuilder(new IdmLightApplication(iidMStore, claimCache))
+ .build())
+ .addUrlPattern("/*")
+ .build())
// Allows user to add javax.servlet.Filter(s) in front of REST services
- .addFilter(FilterDetails.builder().filter(new CustomFilterAdapter(customFilterAdapterConfig))
- .addUrlPattern("/*").build());
+ .addFilter(FilterDetails.builder()
+ .filter(new CustomFilterAdapter(customFilterAdapterConfig))
+ .addUrlPattern("/*")
+ .build());
webContextSecurer.requireAuthentication(webContextBuilder, "/*", "/moon/*");
<packaging>bundle</packaging>
<dependencies>
-
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
</dependency>
<dependency>
- <groupId>org.immutables</groupId>
- <artifactId>value</artifactId>
- <classifier>annotations</classifier>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.opendaylight.yangtools</groupId>
*/
package org.opendaylight.aaa.web;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
-import org.immutables.value.Value;
-import org.immutables.value.Value.Default;
+import org.eclipse.jdt.annotation.NonNull;
/**
* Details about a {@link Filter}.
*
* @author Michael Vorburger.ch
*/
-@Value.Immutable
-@Value.Style(visibility = Value.Style.ImplementationVisibility.PRIVATE, depluralize = true)
public interface FilterDetails {
+ /**
+ * Get a {@link Filter} instance.
+ *
+ * @return {@link Filter} instance
+ */
+ @NonNull Filter filter();
- static FilterDetailsBuilder builder() {
- return new FilterDetailsBuilder();
- }
+ /**
+ * Get Filter's name.
+ *
+ * @return {@link String}
+ */
+ @NonNull String name();
+
+ /**
+ * Get list of Filter URL patterns. These patterns control where filter is applied.
+ *
+ * <p>
+ * Restrictions to URLs and how it should look like are next:
+ * <ul>
+ * <li>A string beginning with a ‘ / ’ character and ending with a ‘ /*’ suffix is used for path mapping.</li>
+ * <li>A string beginning with a ‘ *. ’ prefix is used as an extension mapping.</li>
+ * <li>The empty string ("") is a special URL pattern that exactly maps to the application's context root, i.e.,
+ * requests of the form {@code http://host:port/context-root/}. In this case the path info is ’ / ’ and the
+ * servlet path and context path is empty string (““).</li>
+ * <li>A string containing only the ’ / ’ character indicates the "default" servlet of the application. In this
+ * case the servlet path is the request URI minus the context path and the path info is null.</li>
+ * <li>All other strings are used for exact matches only.</li>
+ * </ul>
+ *
+ * @return {@link List} of Filter URL patterns
+ * @see "Java Servlet Specification Version 3.1, Section 12.2 Specification of Mappings"
+ */
+ @NonNull List<String> urlPatterns();
- Filter filter();
+ /**
+ * Get Filter initial parameters.
+ *
+ * @return {@link Map} that contains initial parameters
+ */
+ @NonNull Map<String, String> initParams();
- @Default default String name() {
- return filter().getClass().getName();
+ /**
+ * Get indication whether {@link #filter()} supports asynchronous processing.
+ *
+ * @return {@code true} if the filter supports asynchronous processing
+ * @see "Java Servlet Specification Version 3.1, Section 2.3.3.3 Asynchronous Processing"
+ */
+ boolean asyncSupported();
+
+ /**
+ * Create a builder for {@link FilterDetails}.
+ *
+ * @return {@link Builder} builder instance
+ */
+ static @NonNull Builder builder() {
+ return new Builder();
}
- List<String> urlPatterns();
+ /**
+ * Builds instances of type {@link FilterDetails FilterDetails}. Initialize attributes and then invoke the
+ * {@link #build()} method to create an immutable instance.
+ *
+ * <p><em>{@code FilterDetails.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 ImmutableFilterDetails(Filter filter, String name, ImmutableList<String> urlPatterns,
+ ImmutableMap<String, String> initParams, boolean asyncSupported) implements FilterDetails {
+ ImmutableFilterDetails {
+ if (urlPatterns.isEmpty()) {
+ throw new IllegalStateException("No urlPattern specified");
+ }
+ }
+ }
- Map<String, String> initParams();
+ private final ImmutableMap.Builder<String, String> initParams = ImmutableMap.builder();
+ private final ImmutableList.Builder<String> urlPatterns = ImmutableList.builder();
+ private Filter filter;
+ private String name;
+ private boolean asyncSupported;
- @Default default Boolean getAsyncSupported() {
- return false;
- }
+ private Builder() {
+ // Hidden on purpose
+ }
+
+ /**
+ * Initializes the value for the {@link FilterDetails#filter() filter} attribute.
+ *
+ * @param filter The value for filter
+ * @return {@code this} builder for use in a chained invocation
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder filter(final Filter filter) {
+ this.filter = requireNonNull(filter);
+ return this;
+ }
+ /**
+ * Initializes the value for the {@link FilterDetails#name() name} attribute.
+ *
+ * <p><em>If not set, this attribute will have a value corresponding to {@code filter().getClass().getName()}.
+ * </em>
+ *
+ * @param name The value for name
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if {code name} is {@code null}
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder name(final String name) {
+ this.name = requireNonNull(name);
+ return this;
+ }
+
+ /**
+ * Adds one element to {@link FilterDetails#urlPatterns() urlPatterns} list.
+ *
+ * @param urlPattern A urlPatterns element
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if {code urlPattern} is {@code null}
+ * @throws IllegalArgumentException if {@code urlPattern} does not meet specification criteria
+ */
+ public @NonNull Builder addUrlPattern(final String urlPattern) {
+ urlPatterns.add(ServletSpec.requireMappingSpec(urlPattern));
+ return this;
+ }
+
+ /**
+ * Put one entry to the {@link FilterDetails#initParams() initParams} map.
+ *
+ * @param key The key in the initParams map
+ * @param value The associated value in the initParams map
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public @NonNull Builder putInitParam(final String key, final String value) {
+ initParams.put(key, value);
+ return this;
+ }
+
+ /**
+ * Initializes the value for the {@link FilterDetails#asyncSupported() asyncSupported} attribute.
+ *
+ * <p><em>If not set, this attribute will have a default value of {@code false}.</em>
+ *
+ * @param asyncSupported The value for asyncSupported
+ * @return {@code this} builder for use in a chained invocation
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder asyncSupported(final boolean asyncSupported) {
+ this.asyncSupported = asyncSupported;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link FilterDetails FilterDetails}.
+ *
+ * @return An immutable instance of FilterDetails
+ * @throws IllegalStateException if any required attributes are missing
+ */
+ public @NonNull FilterDetails build() {
+ if (filter == null) {
+ throw new IllegalStateException("No filter specified");
+ }
+ return new ImmutableFilterDetails(filter, name != null ? name : filter.getClass().getName(),
+ urlPatterns.build(), initParams.build(), asyncSupported);
+ }
+ }
}
*/
package org.opendaylight.aaa.web;
-import org.immutables.value.Value;
-import org.immutables.value.Value.Default;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNull;
/**
* Details about a resource registration.
*
* @author Thomas Pantelis
*/
-@Value.Immutable
-@Value.Style(visibility = Value.Style.ImplementationVisibility.PRIVATE, depluralize = true)
public interface ResourceDetails {
-
- static ResourceDetailsBuilder builder() {
- return new ResourceDetailsBuilder();
- }
-
/**
+ * Get resource base name.
+ *
+ * <p>
* The base name of the resources that will be registered, typically a directory in the bundle/jar where "/"
* is used to denote the root.
+ *
+ * @return {@link String} base name
*/
- String name();
+ @NonNull String name();
/**
+ * Get resource mapped alias.
+ *
+ * <p>
* The name in the URI namespace to which the resources are mapped. This defaults to the {@link #name()}.
+ *
+ * @return {@link String} mapped alias
+ */
+ @NonNull String alias();
+
+ /**
+ * Create builder for {@code ResourceDetails}.
+ *
+ * @return {@link Builder} builder instance
+ */
+ static @NonNull Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builds instances of type {@link ResourceDetails ResourceDetails}. Initialize attributes and then invoke the
+ * {@link #build()} method to create an immutable instance.
+ *
+ * <p><em>{@code ResourceDetails.Builder} is not thread-safe and generally should not be stored in a field or
+ * collection, but instead used immediately to create instances.</em>
*/
- @Default default String alias() {
- return name();
+ final class Builder {
+ private record ImmutableResourceDetails(String name, String alias) implements ResourceDetails {
+ // Not much else here
+ }
+
+ private String name;
+ private String alias;
+
+ private Builder() {
+ // Hidden on purpose
+ }
+
+ /**
+ * Initializes the value for the {@link ResourceDetails#name() name} attribute.
+ *
+ * @param name The value for name
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if {code name} is {@code null}
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder name(final String name) {
+ this.name = requireNonNull(name);
+ return this;
+ }
+
+ /**
+ * Initializes the value for the {@link ResourceDetails#alias() alias} attribute.
+ *
+ * <p><em>If not set, this attribute will have the same as {@link ResourceDetails#name() name}.</em>
+ *
+ * @param alias The value for alias
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if {code alias} is {@code null}
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder alias(final String alias) {
+ this.alias = requireNonNull(alias);
+ return this;
+ }
+
+ /**
+ * Builds a new {@link ResourceDetails ResourceDetails}.
+ *
+ * @return An immutable instance of ResourceDetails
+ * @throws IllegalStateException if any required attributes are missing
+ */
+ public @NonNull ResourceDetails build() {
+ if (name == null) {
+ throw new IllegalStateException("name not specified");
+ }
+ return new ImmutableResourceDetails(name, alias == null ? name : alias);
+ }
}
}
*/
package org.opendaylight.aaa.web;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Servlet;
-import org.immutables.value.Value;
-import org.immutables.value.Value.Default;
+import org.eclipse.jdt.annotation.NonNull;
/**
* Details about a {@link Servlet}.
*
* @author Michael Vorburger.ch
*/
-@Value.Immutable
-@Value.Style(visibility = Value.Style.ImplementationVisibility.PRIVATE, depluralize = true)
public interface ServletDetails {
+ /**
+ * Get a {@link Servlet} instance.
+ *
+ * @return {@link Servlet} instance
+ */
+ @NonNull Servlet servlet();
- static ServletDetailsBuilder builder() {
- return new ServletDetailsBuilder();
- }
+ /**
+ * Get Servlet's name.
+ *
+ * @return {@link String} servlet name
+ */
+ @NonNull String name();
+
+ /**
+ * Get list of servlet URL patterns. These patterns control how you access a servlet.
+ *
+ * <p>
+ * Restrictions to URLs and how it should look like are next:
+ * <ul>
+ * <li>A string beginning with a ‘ / ’ character and ending with a ‘ /*’ suffix is used for path mapping.</li>
+ * <li>A string beginning with a ‘ *. ’ prefix is used as an extension mapping.</li>
+ * <li>The empty string ("") is a special URL pattern that exactly maps to the application's context root, i.e.,
+ * requests of the form {@code http://host:port/context-root}. In this case the path info is ’ / ’ and the
+ * servlet path and context path is empty string (““).</li>
+ * <li>A string containing only the ’ / ’ character indicates the "default" servlet of the application. In this
+ * case the servlet path is the request URI minus the context path and the path info is null.</li>
+ * <li>All other strings are used for exact matches only.</li>
+ * </ul>
+ *
+ * @return {@link List} of Servlet URL patterns
+ * @see "Java Servlet Specification Version 3.1, Section 12.2 Specification of Mappings"
+ */
+ @NonNull List<String> urlPatterns();
- Servlet servlet();
+ /**
+ * Get Servlet initial parameters.
+ *
+ * @return {@link Map} that contains initial parameters
+ */
+ @NonNull Map<String, String> initParams();
- @Default default String name() {
- return servlet().getClass().getName();
+ /**
+ * Get indication whether {@link #servlet()} supports asynchronous processing.
+ *
+ * @return {@code true} if the filter supports asynchronous processing
+ * @see "Java Servlet Specification Version 3.1, Section 2.3.3.3 Asynchronous Processing"
+ */
+ boolean asyncSupported();
+
+ /**
+ * Create a builder for {@link ServletDetails}.
+ *
+ * @return {@link Builder} builder instance
+ */
+ static @NonNull Builder builder() {
+ return new Builder();
}
- List<String> urlPatterns();
+ /**
+ * Builds instances of type {@link ServletDetails ServletDetails}. Initialize attributes and then invoke the
+ * {@link #build()} method to create an immutable instance.
+ *
+ * <p><em>{@code ServletDetails.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 ImmutableServletDetails(Servlet servlet, String name, ImmutableList<String> urlPatterns,
+ ImmutableMap<String, String> initParams, boolean asyncSupported) implements ServletDetails {
+ ImmutableServletDetails {
+ if (urlPatterns.isEmpty()) {
+ throw new IllegalStateException("No urlPattern specified");
+ }
+ }
+ }
+
+ private final ImmutableMap.Builder<String, String> initParams = ImmutableMap.builder();
+ private final ImmutableList.Builder<String> urlPatterns = ImmutableList.builder();
+ private Servlet servlet;
+ private String name;
+ private boolean asyncSupported;
+
+ private Builder() {
+ // Hidden on purpose
+ }
+
+ /**
+ * Initializes the value for the {@link ServletDetails#servlet() servlet} attribute.
+ *
+ * @param servlet The value for servlet
+ * @return {@code this} builder for use in a chained invocation
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder servlet(final Servlet servlet) {
+ this.servlet = requireNonNull(servlet);
+ return this;
+ }
+
+ /**
+ * Initializes the value for the {@link ServletDetails#name() name} attribute.
+ *
+ * <p><em>If not set, this attribute will have a value corresponding to {@code servlet().getClass().getName()}.
+ * </em>
+ *
+ * @param name The value for name
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if {code name} is {@code null}
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder name(final String name) {
+ this.name = requireNonNull(name);
+ return this;
+ }
+
+ /**
+ * Adds one element to {@link ServletDetails#urlPatterns() urlPatterns} list.
+ *
+ * @param urlPattern A urlPatterns element
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if {code urlPattern} is {@code null}
+ * @throws IllegalArgumentException if {@code urlPattern} does not meet specification criteria
+ */
+ public @NonNull Builder addUrlPattern(final String urlPattern) {
+ urlPatterns.add(ServletSpec.requireMappingSpec(urlPattern));
+ return this;
+ }
+
+ /**
+ * Put one entry to the {@link ServletDetails#initParams() initParams} map.
+ *
+ * @param key The key in the initParams map
+ * @param value The associated value in the initParams map
+ * @return {@code this} builder for use in a chained invocation
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public @NonNull Builder putInitParam(final String key, final String value) {
+ initParams.put(key, value);
+ return this;
+ }
- Map<String, String> initParams();
+ /**
+ * Initializes the value for the {@link ServletDetails#asyncSupported() asyncSupported} attribute.
+ *
+ * <p><em>If not set, this attribute will have a default value of {@code false}.</em>
+ *
+ * @param asyncSupported The value for asyncSupported
+ * @return {@code this} builder for use in a chained invocation
+ */
+ @SuppressWarnings("checkstyle:hiddenField")
+ public @NonNull Builder asyncSupported(final boolean asyncSupported) {
+ this.asyncSupported = asyncSupported;
+ return this;
+ }
- @Default default Boolean getAsyncSupported() {
- return false;
+ /**
+ * Builds a new {@link ServletDetails ServletDetails}.
+ *
+ * @return An immutable instance of ServletDetails
+ * @throws IllegalStateException if any required attributes are missing
+ */
+ public @NonNull ServletDetails build() {
+ if (servlet == null) {
+ throw new IllegalStateException("No servlet specified");
+ }
+ return new ImmutableServletDetails(servlet, name != null ? name : servlet.getClass().getName(),
+ urlPatterns.build(), initParams.build(), asyncSupported);
+ }
}
}
--- /dev/null
+/*
+ * 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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Utility methods for dealing with aspects of Java Servlet Specification. We currently support
+ * <a href="https://github.com/javaee/servlet-spec/blob/gh-pages/downloads/servlet-3.1/Final/servlet-3_1-final.pdf">
+ * version 3.1</a>.
+ */
+final class ServletSpec {
+ private ServletSpec() {
+ // utility class
+ }
+
+ /**
+ * Verify that the specified string is a valid Context Path as defined in Section 3.5.
+ *
+ * @param str String to check
+ * @return The string
+ * @throws IllegalArgumentException if {@code str} is not a valid context path
+ * @throws NullPointerException if {@code str} is {@code null}
+ */
+ static @NonNull String requireContextPath(final String str) {
+ // We do not allow this:
+ // If this context is the “default” context rooted at the base of the
+ // Web server’s URL name space, this path will be an empty string.
+ checkArgument(!str.isEmpty(), "Context path is empty");
+
+ // Otherwise, if the
+ // context is not rooted at the root of the server’s name space, the path starts with a
+ // character but does not end with a / character.
+ checkArgument(str.charAt(0) == '/', "Context path '%s' does not start with '/'", str);
+ checkArgument(str.charAt(str.length() - 1) != '/', "Context path '%s' ends with '/'", str);
+
+ // TODO: validate according to https://www.rfc-editor.org/rfc/rfc3986#section-3.3
+
+ return str;
+ }
+
+ /**
+ * Verify that the specified string is a valid Specification of Mapping as defined in Section 12.2.
+ *
+ * @param str String to check
+ * @return The string
+ * @throws IllegalArgumentException if {@code str} is not a valid mapping specification
+ * @throws NullPointerException if {@code str} is {@code null}
+ */
+ static @NonNull String requireMappingSpec(final String str) {
+ // Bullet 3:
+ // The empty string ("") is a special URL pattern that exactly maps to the
+ // application's context root, i.e., requests of the form http://host:port/<context-
+ // root>/. In this case the path info is ’ / ’ and the servlet path and context path is
+ // empty string (““).
+ if (str.isEmpty()) {
+ return "";
+ }
+
+ final char firstChar = str.charAt(0);
+ final int len = str.length();
+ if (firstChar == '/') {
+ // Bullet 4:
+ // A string containing only the ’ / ’ character indicates the "default" servlet of the
+ // application. In this case the servlet path is the request URI minus the context path
+ // and the path info is null.
+ // otherwise ...
+ if (len != 1) {
+ // ... more checks starting at the second character
+ final int star = str.indexOf('*', 1);
+ checkArgument(
+ // Bullet 5:
+ // All other strings are used for exact matches only.
+ star == -1
+ // or Bullet 1:
+ // A string beginning with a ‘ / ’ character and ending with a ‘ /*’ suffix is used for
+ // path mapping.
+ || star == len - 1 && str.charAt(star - 1) == '/',
+ // ... otherwise it is a '*' in an exact path
+ "Prefix-based spec '%s' with a '*' at offset %s", str, star);
+ }
+ } else {
+ // Bullet 2:
+ // A string beginning with a ‘ *. ’ prefix is used as an extension mapping
+ checkArgument(firstChar == '.' && len > 1 && str.charAt(1) == '*',
+ "Spec '%s' is neither prefix-based nor suffix-based", str);
+
+ final int slash = str.indexOf('/', 2);
+ checkArgument(slash == -1, "Suffix-based spec '%s' with a '/' at offset %s", str, slash);
+ final int star = str.indexOf('*', 2);
+ checkArgument(star == -1, "Suffix-based spec '%s' with a '*' at offset %s", str, star);
+ }
+
+ return str;
+ }
+}
*/
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);
+ }
}
}
package org.opendaylight.aaa.web;
/**
- * Secures a {@link WebContextBuilder}.
+ * Secures a {@link WebContext.Builder}.
*
* @author Michael Vorburger.ch
*/
public interface WebContextSecurer {
/**
- * Configures the WebContext in an implementation specific manner so that it requires authentication to access the
+ * Configure the WebContext to require auth for specified URLs.
+ *
+ * <p>
+ * Configure the WebContext so that it requires authentication to access the
* given URL Patterns. Typically, this will be done by adding a {@code javax.servlet.Filter} (or several, and
* whatever else they need).
*
* @param asyncSupported true if asynchronous communication should also be supported
* @param urlPatterns URL patterns that require authentication
*/
- void requireAuthentication(WebContextBuilder webContextBuilder, boolean asyncSupported, String... urlPatterns);
+ void requireAuthentication(WebContext.Builder webContextBuilder, boolean asyncSupported, String... urlPatterns);
/**
- * Configures the WebContext in an implementation specific manner so that it requires authentication to access the
+ * Configure the WebContext to require auth for specified URLs.
+ *
+ * <p>
+ * Configures the WebContext so that it requires authentication to access the
* given URL Patterns. Typically, this will be done by adding a {@code javax.servlet.Filter} (or several, and
* whatever else they need).
*
* <p>
* This method is equivalent to {@code requireAuthentication(webContextBuilder, false, urlPatterns}.
+ *
+ * @param webContextBuilder builder to secure
+ * @param urlPatterns URL patterns that require authentication
*/
- default void requireAuthentication(final WebContextBuilder webContextBuilder, final String... urlPatterns) {
+ default void requireAuthentication(final WebContext.Builder webContextBuilder, final String... urlPatterns) {
requireAuthentication(webContextBuilder, false, urlPatterns);
}
/**
+ * Configure the WebContext to require auth all URLs.
+ *
+ * <p>
* Configures the WebContext so that all its URL patterns ({@code/**}) require authentication.
- * @see #requireAuthentication(WebContextBuilder, String...)
+ *
+ * @param webContextBuilder builder to secure
+ * @see #requireAuthentication(WebContext.Builder, String...)
*/
- default void requireAuthentication(final WebContextBuilder webContextBuilder) {
+ default void requireAuthentication(final WebContext.Builder webContextBuilder) {
requireAuthentication(webContextBuilder, "/*");
}
}
Registration 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
+ * Get base URL of this web server, without any contexts.
+ *
+ * <p>
+ * In production, this would likely be HTTPS with a well known hostname and fixed port configured.
+ * For example, in 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
*/
package org.opendaylight.aaa.web;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class FilterDetailsTest {
@Test
public void testDefaultValue() {
- FilterDetails filterDetails = FilterDetails.builder()
- .filter(mock(Filter.class))
- .addUrlPattern("test")
- .addUrlPattern("another")
- .name("custom")
- .putInitParam("key", "value")
- .build();
+ var filterDetails = FilterDetails.builder()
+ .filter(mock(Filter.class))
+ .addUrlPattern("/test")
+ .addUrlPattern("/another")
+ .name("custom")
+ .putInitParam("key", "value")
+ .build();
- assertFalse(filterDetails.getAsyncSupported());
+ assertFalse(filterDetails.asyncSupported());
}
@Test
public void testAsyncFalse() {
- FilterDetails filterDetails = FilterDetails.builder()
+ var filterDetails = FilterDetails.builder()
.filter(mock(Filter.class))
- .addUrlPattern("test")
- .addUrlPattern("another")
+ .addUrlPattern("/test")
+ .addUrlPattern("/another")
.name("custom")
.putInitParam("key", "value")
.asyncSupported(false)
.build();
- assertFalse(filterDetails.getAsyncSupported());
+ assertFalse(filterDetails.asyncSupported());
}
@Test
public void testAsyncTrue() {
- FilterDetails filterDetails = FilterDetails.builder()
- .filter(mock(Filter.class))
- .addUrlPattern("test")
- .addUrlPattern("another")
- .name("custom")
- .putInitParam("key", "value")
- .asyncSupported(true)
- .build();
+ var filterDetails = FilterDetails.builder()
+ .filter(mock(Filter.class))
+ .addUrlPattern("/test")
+ .addUrlPattern("/another")
+ .name("custom")
+ .putInitParam("key", "value")
+ .asyncSupported(true)
+ .build();
+
+ assertTrue(filterDetails.asyncSupported());
+ }
- assertTrue(filterDetails.getAsyncSupported());
+ @Test
+ public void testEmptyBuilderException() {
+ final var builder = FilterDetails.builder();
+ final var ex = assertThrows(IllegalStateException.class, builder::build);
+ assertEquals("No filter specified", ex.getMessage());
+ }
+
+ @Test
+ public void testBadFilterWithoutAnyURL() {
+ final var builder = FilterDetails.builder().filter(mock(Filter.class));
+ final var ex = assertThrows(IllegalStateException.class, builder::build);
+ assertEquals("No urlPattern specified", ex.getMessage());
+ }
+
+ @Test
+ public void testNotPrefixNorSuffixPatternException() {
+ final var builder = FilterDetails.builder();
+ final var ex = assertThrows(IllegalArgumentException.class, () -> builder.addUrlPattern("test"));
+ assertEquals("Spec 'test' is neither prefix-based nor suffix-based", ex.getMessage());
+ }
+
+ @Test
+ public void testIllegalPrefixPatternException() {
+ final var builder = FilterDetails.builder();
+ final var ex = assertThrows(IllegalArgumentException.class, () -> builder.addUrlPattern("/*test"));
+ assertEquals("Prefix-based spec '/*test' with a '*' at offset 1", ex.getMessage());
}
@Test
- public void testException() {
- assertThrows(IllegalStateException.class, () -> FilterDetails.builder().build());
+ public void testIllegalSuffixPatternException() {
+ final var builder = FilterDetails.builder();
+ final var ex = assertThrows(IllegalArgumentException.class, () -> builder.addUrlPattern("*./test"));
+ assertEquals("Spec '*./test' is neither prefix-based nor suffix-based", ex.getMessage());
}
}
*/
package org.opendaylight.aaa.web;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ServletDetailsTest {
@Test
public void testDefaultValue() {
- ServletDetails servletDetails = ServletDetails.builder()
- .servlet(mock(Servlet.class))
- .addUrlPattern("test")
- .addUrlPattern("another")
- .name("custom")
- .putInitParam("key", "value")
- .build();
+ final var servletDetails = ServletDetails.builder()
+ .servlet(mock(Servlet.class))
+ .addUrlPattern("/test")
+ .addUrlPattern("/another")
+ .name("custom")
+ .putInitParam("key", "value")
+ .build();
- assertFalse(servletDetails.getAsyncSupported());
+ assertFalse(servletDetails.asyncSupported());
}
@Test
public void testAsyncFalse() {
- ServletDetails servletDetails = ServletDetails.builder()
- .servlet(mock(Servlet.class))
- .addUrlPattern("test")
- .addUrlPattern("another")
- .name("custom")
- .putInitParam("key", "value")
- .asyncSupported(false)
- .build();
+ final var servletDetails = ServletDetails.builder()
+ .servlet(mock(Servlet.class))
+ .addUrlPattern("/test")
+ .addUrlPattern("/another")
+ .name("custom")
+ .putInitParam("key", "value")
+ .asyncSupported(false)
+ .build();
- assertFalse(servletDetails.getAsyncSupported());
+ assertFalse(servletDetails.asyncSupported());
}
@Test
public void testAsyncTrue() {
- ServletDetails servletDetails = ServletDetails.builder()
- .servlet(mock(Servlet.class))
- .addUrlPattern("test")
- .addUrlPattern("another")
- .name("custom")
- .putInitParam("key", "value")
- .asyncSupported(true)
- .build();
+ final var servletDetails = ServletDetails.builder()
+ .servlet(mock(Servlet.class))
+ .addUrlPattern("/test")
+ .addUrlPattern("/another")
+ .name("custom")
+ .putInitParam("key", "value")
+ .asyncSupported(true)
+ .build();
- assertTrue(servletDetails.getAsyncSupported());
+ assertTrue(servletDetails.asyncSupported());
}
@Test
- public void testException() {
- assertThrows(IllegalStateException.class, () -> ServletDetails.builder().build());
+ public void testEmptyBuilderException() {
+ final var builder = ServletDetails.builder();
+ final var ex = assertThrows(IllegalStateException.class, builder::build);
+ assertEquals("No servlet specified", ex.getMessage());
+ }
+
+ @Test
+ public void testBadServletWithoutAnyURL() {
+ final var builder = ServletDetails.builder().servlet(mock(Servlet.class));
+ final var ex = assertThrows(IllegalStateException.class, builder::build);
+ assertEquals("No urlPattern specified", ex.getMessage());
}
}
import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletContextListener;
-import org.junit.Ignore;
import org.junit.Test;
/**
public class WebContextApiTest {
@Test
public void testEmptyBuilder() {
- final WebContextBuilder builder = WebContext.builder();
- assertThrows(IllegalStateException.class, () -> builder.build());
+ final var builder = WebContext.builder();
+ assertThrows(IllegalStateException.class, builder::build);
}
@Test
public void testMinimalBuilder() {
- assertTrue(WebContext.builder().contextPath("test").build().supportsSessions());
- assertEquals("test", WebContext.builder().contextPath("test").supportsSessions(false).build().contextPath());
+ assertTrue(WebContext.builder().contextPath("/test").build().supportsSessions());
+ assertEquals("/test", WebContext.builder().contextPath("/test").supportsSessions(false).build().contextPath());
}
@Test
public void testAddSimpleServlet() {
- WebContext webContext = WebContext.builder().contextPath("test")
- .addServlet(ServletDetails.builder().servlet(mock(Servlet.class)).addUrlPattern("test").build())
+ 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);
@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())
+ 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();
+ 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()
+ assertThat(WebContext.builder().contextPath("/test").addListener(mock(ServletContextListener.class)).build()
.listeners(), hasSize(1));
}
@Test
public void testContextParam() {
assertEquals(Map.of("key", "value"),
- WebContext.builder().contextPath("test").putContextParam("key", "value").build().contextParams());
+ WebContext.builder().contextPath("/test").putContextParam("key", "value").build().contextParams());
}
@Test
- @Ignore
public void testBadContextPath() {
- // FIXME: this is completely broken usage -- which call is expected to raise the exception?!
- 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());
+ assertBadContextPath("Context path is empty", "");
+ assertBadContextPath("Context path 'test/sub' does not start with '/'", "test/sub");
+ assertBadContextPath("Context path 'test space' does not start with '/'", "test space");
+ assertBadContextPath("Context path 'test/' does not start with '/'", "test/");
+ assertBadContextPath("Context path '/test/' ends with '/'", "/test/");
}
- @Test
- public void testBadServletWithoutAnyURL() {
- final WebContextBuilder builder = WebContext.builder().contextPath("test")
- .addServlet(ServletDetails.builder().servlet(mock(Servlet.class)).build());
- assertThrows(IllegalArgumentException.class, () -> builder.build());
+ private static void assertBadContextPath(final String expectedMessage, final String contextPath) {
+ final var builder = WebContext.builder();
+ final var ex = assertThrows(IllegalArgumentException.class, () -> builder.contextPath(contextPath));
+ assertEquals(expectedMessage, ex.getMessage());
}
- @Test
- public void testBadFilterWithoutAnyURL() {
- final WebContextBuilder builder = WebContext.builder().contextPath("test")
- .addFilter(FilterDetails.builder().filter(mock(Filter.class)).build());
- assertThrows(IllegalArgumentException.class, () -> builder.build());
- }
}
@Override
public synchronized Registration registerWebContext(final WebContext webContext) throws ServletException {
- String contextPathWithSlashPrefix = ensureAbsolutePath(webContext.contextPath());
- ServletContextHandler handler = new ServletContextHandler(contextHandlerCollection, contextPathWithSlashPrefix,
+ ServletContextHandler handler = new ServletContextHandler(contextHandlerCollection, webContext.contextPath(),
webContext.supportsSessions() ? ServletContextHandler.SESSIONS : ServletContextHandler.NO_SESSIONS);
// The order in which we do things here must be the same as
FilterHolder filterHolder = new FilterHolder(filter.filter());
filterHolder.setInitParameters(filter.initParams());
filter.urlPatterns().forEach(
- urlPattern -> handler.addFilter(filterHolder, ensureAbsolutePath(urlPattern),
+ urlPattern -> handler.addFilter(filterHolder, urlPattern,
EnumSet.allOf(DispatcherType.class))
);
});
webContext.servlets().forEach(servlet -> {
ServletHolder servletHolder = new ServletHolder(servlet.name(), servlet.servlet());
servletHolder.setInitParameters(servlet.initParams());
- servletHolder.setAsyncSupported(servlet.getAsyncSupported());
+ servletHolder.setAsyncSupported(servlet.asyncSupported());
// AKA <load-on-startup> 1
servletHolder.setInitOrder(1);
servlet.urlPatterns().forEach(
- urlPattern -> handler.addServlet(servletHolder, ensureAbsolutePath(urlPattern))
+ urlPattern -> handler.addServlet(servletHolder, urlPattern)
);
});
}
contextHandlerCollection.removeHandler(handler);
}
-
- private static String ensureAbsolutePath(final String path) {
- return path.startsWith("/") ? path : "/" + path;
- }
}
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.google.common.io.CharStreams;
}
@Test
- public void testAddAfterStartWithoutSlashOnContext() throws ServletException, IOException {
- // NB subtle difference to previous test: contextPath("test1") instead of /test1 with slash!
- var webContext = WebContext.builder()
- .contextPath("test1")
- .addServlet(ServletDetails.builder().addUrlPattern("/*").name("Test").servlet(new TestServlet()).build())
- .build();
- try (var webContextRegistration = getWebServer().registerWebContext(webContext)) {
- checkTestServlet(getWebServer().getBaseURL() + "/test1");
- }
+ public void testAddAfterStartWithoutSlashOnServlet() throws ServletException, IOException {
+ // NB subtle difference to testAddAfterStart() test: addUrlPattern("*") instead of /* with slash!
+ var builder = WebContext.builder()
+ .contextPath("/test1");
+ assertThrows(IllegalArgumentException.class, () -> builder
+ .addServlet(ServletDetails.builder().addUrlPattern("*").name("Test").servlet(new TestServlet()).build()));
}
@Test
}
}
+ @Test
+ public void testAddFilterWithoutSlash() throws Exception {
+ // NB subtle difference to previous test: addUrlPattern("*") instead of /* with slash!
+ var testFilter = new TestFilter();
+ var builder = WebContext.builder()
+ .contextPath("/testingFilters")
+ .putContextParam("testParam1", "avalue");
+ assertThrows(IllegalArgumentException.class, () -> builder
+ .addFilter(FilterDetails.builder().addUrlPattern("*").name("Test").filter(testFilter).build()));
+ }
+
@Test
public void testRegisterListener() throws Exception {
var testListener = new TestListener();
// 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());
+ final var contextPath = webContext.contextPath();
// TODO: can we create a better name?
final var contextName = contextPath + ".id";
private static Map<String, Object> filterProperties(final String contextSelect, final FilterDetails filter) {
final var builder = ImmutableMap.<String, Object>builder()
.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, contextSelect)
- .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_ASYNC_SUPPORTED, filter.getAsyncSupported())
+ .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_ASYNC_SUPPORTED, filter.asyncSupported())
.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_NAME, filter.name())
.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, absolutePatterns(filter.urlPatterns()));
private static Map<String, Object> servletProperties(final String contextSelect, final ServletDetails servlet) {
final var builder = ImmutableMap.<String, Object>builder()
.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, contextSelect)
- .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED, servlet.getAsyncSupported())
+ .put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED, servlet.asyncSupported())
.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, servlet.name())
.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, absolutePatterns(servlet.urlPatterns()));