diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 5c05554e..bac9279c 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,7 @@ +## 3.0-M5 + +* #125 Unified API for mapping static servlets + ## 3.0-M4 * #124 Upgrade to Jetty 11.0.20 and 10.0.20 diff --git a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/JettyModuleExtender.java b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/JettyModuleExtender.java index 0acf2a52..1a96949e 100644 --- a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/JettyModuleExtender.java +++ b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/JettyModuleExtender.java @@ -24,7 +24,6 @@ import io.bootique.di.*; import io.bootique.jetty.request.RequestMDCItem; import io.bootique.jetty.server.ServletContextHandlerExtender; -import io.bootique.jetty.servlet.MultiBaseStaticServlet; import jakarta.servlet.Filter; import jakarta.servlet.Servlet; @@ -104,9 +103,7 @@ public JettyModuleExtender addListener(Class listenerTy } /** - * @param mappedListener - * @param - * @return + * @return this extender instance */ public JettyModuleExtender addMappedListener(MappedListener mappedListener) { contributeMappedListeners().addInstance(mappedListener); @@ -114,9 +111,7 @@ public JettyModuleExtender addMappedListener(MappedLis } /** - * @param mappedListenerKey - * @param - * @return + * @return this extender instance */ public JettyModuleExtender addMappedListener(Key> mappedListenerKey) { contributeMappedListeners().add(mappedListenerKey); @@ -124,20 +119,26 @@ public JettyModuleExtender addMappedListener(Key * @return this extender instance */ public JettyModuleExtender addMappedListener(TypeLiteral> mappedListenerType) { return addMappedListener(Key.get(mappedListenerType)); } + /** + * @deprecated in favor of {@link #addMappedServlet(MappedServlet)} with {@link MappedServlet#ofStatic(String)} + */ + @Deprecated(since = "3.0", forRemoval = true) public JettyModuleExtender addStaticServlet(String name, String... urlPatterns) { - return addServlet(new MultiBaseStaticServlet(), name, urlPatterns); + return addMappedServlet(MappedServlet.ofStatic(name).urlPatterns(urlPatterns).build()); } + /** + * @deprecated in favor of {@link #addMappedServlet(MappedServlet)} with {@link MappedServlet#ofStatic(String)} + */ + @Deprecated(since = "3.0", forRemoval = true) public JettyModuleExtender useDefaultServlet() { - return addStaticServlet("default", "/"); + return addMappedServlet(MappedServlet.ofStatic("default").urlPatterns("/").build()); } /** diff --git a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/MappedServlet.java b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/MappedServlet.java index c610cb61..084b7022 100644 --- a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/MappedServlet.java +++ b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/MappedServlet.java @@ -1,63 +1,131 @@ -/** - * Licensed to ObjectStyle LLC under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ObjectStyle LLC licenses - * this file to you under the Apache License, Version 2.0 (the - * “License”); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at +/* + * Licensed to ObjectStyle LLC under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ObjectStyle LLC licenses + * this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package io.bootique.jetty; +import io.bootique.jetty.servlet.MultiBaseStaticServlet; +import io.bootique.resource.FolderResourceFactory; import jakarta.servlet.Servlet; import java.util.Collections; import java.util.Map; import java.util.Set; +/** + * A wrapper around a servlet object that provides access to its URL mapping and parameters. + */ public class MappedServlet extends MappedWebArtifact { - public MappedServlet(T servlet, Set urlPatterns) { - this(servlet, urlPatterns, null); - } - - /** - * @param servlet - * underlying servlet instance. - * @param urlPatterns - * URL patterns that this servlet will respond to. - * @param name - * servlet name. If null, Jetty will assign its own name. - */ - public MappedServlet(T servlet, Set urlPatterns, String name) { - this(servlet, urlPatterns, name, Collections.emptyMap()); - } - - /** - * @param servlet - * underlying servlet instance. - * @param urlPatterns - * URL patterns that this servlet will respond to. - * @param name - * servlet name. If null, Jetty will assign its own name. - * @param params - * servlet init parameters map. - */ - public MappedServlet(T servlet, Set urlPatterns, String name, Map params) { - super(servlet, urlPatterns, name, params); - } - - public T getServlet() { - return getArtifact(); - } + /** + * Creates a builder of a configuration of a "static" MappedServlet that will act as a web server for static + * files located on classpath or in an external folder. + * + * @since 3.0 + */ + public static StaticMappedServletBuilder ofStatic(String name) { + return new StaticMappedServletBuilder(name); + } + + public MappedServlet(T servlet, Set urlPatterns) { + this(servlet, urlPatterns, null); + } + + /** + * @param servlet underlying servlet instance. + * @param urlPatterns URL patterns that this servlet will respond to. + * @param name servlet name. If null, Jetty will assign its own name. + */ + public MappedServlet(T servlet, Set urlPatterns, String name) { + this(servlet, urlPatterns, name, Collections.emptyMap()); + } + + /** + * @param servlet underlying servlet instance. + * @param urlPatterns URL patterns that this servlet will respond to. + * @param name servlet name. If null, Jetty will assign its own name. + * @param params servlet init parameters map. + */ + public MappedServlet(T servlet, Set urlPatterns, String name, Map params) { + super(servlet, urlPatterns, name, params); + } + + public T getServlet() { + return getArtifact(); + } + + /** + * @since 3.0 + */ + public static class StaticMappedServletBuilder { + + private final String name; + private String[] urlPatterns; + private FolderResourceFactory resourceBase; + // capturing this as a String instead of boolean to allow Jetty apply its own string to boolean parsing + // later when our value is mixed with the servlet init params + private String pathInfoOnly; + + protected StaticMappedServletBuilder(String name) { + this.name = name; + } + + /** + * Defines URL patterns for the static servlet. If the call to this method is omitted or the value us null, + * the root pattern will be used ("/"). + */ + public StaticMappedServletBuilder urlPatterns(String... urlPatterns) { + this.urlPatterns = urlPatterns; + return this; + } + + /** + * Sets an optional property that defines the "base" (or "docroot") of the static servlet. This is where the + * files are stored. If not set, either "bq.jetty.staticResourceBase" or "bq.jetty.servlets.[name].params.resourceBase" + * configuration properties must be defined. The latter property, if present, will override the value set here, + * thus allowing to redefine the folder. + * + * @param resourceBase a path or URL of the folder where the static files are stored. Must be in a format + * compatible with Bootique {@link io.bootique.resource.ResourceFactory}. E.g. this may + * be a filesystem path or a "classpath:" URL. + */ + public StaticMappedServletBuilder resourceBase(String resourceBase) { + this.resourceBase = resourceBase != null ? new FolderResourceFactory(resourceBase) : null; + return this; + } + + /** + * Optionally configures the static servlet to ignore the servlet path when resolving URLs to subdirectories. + * Can be overridden via "bq.jetty.servlets.[name].params.pathInfoOnly" configuration property. + */ + public StaticMappedServletBuilder pathInfoOnly() { + this.pathInfoOnly = "true"; + return this; + } + + public MappedServlet build() { + + MultiBaseStaticServlet servlet = new MultiBaseStaticServlet(resourceBase, pathInfoOnly); + Set patterns = this.urlPatterns != null && this.urlPatterns.length > 0 + ? Set.of(this.urlPatterns) + : Set.of("/"); + + return new MappedServlet<>(servlet, patterns, name); + } + } } diff --git a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/MultiBaseStaticServlet.java b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/MultiBaseStaticServlet.java index 021a581a..c0d3345c 100644 --- a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/MultiBaseStaticServlet.java +++ b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/MultiBaseStaticServlet.java @@ -41,9 +41,21 @@ public class MultiBaseStaticServlet extends HttpServlet { private static final Logger LOGGER = LoggerFactory.getLogger(MultiBaseStaticServlet.class); + private final FolderResourceFactory resourceBase; + // capturing this as a String instead of boolean to allow Jetty apply its own string to boolean parsing + private final String pathInfoOnly; + private DoGetProcessor doGetProcessor; private List delegates; + /** + * @since 3.0 + */ + public MultiBaseStaticServlet(FolderResourceFactory resourceBase, String pathInfoOnly) { + this.resourceBase = resourceBase; + this.pathInfoOnly = pathInfoOnly; + } + // overriding methods overridden in the Jetty DefaultServlet to proxy them properly @Override @@ -88,39 +100,50 @@ protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws } protected List createDelegates() { - String resourceBase = getInitParameter(StaticServlet.RESOURCE_BASE_PARAMETER); - if (resourceBase == null) { - return Collections.singletonList(new StaticServlet(null)); - } - Collection resourceBaseUrls = resolveFolderResourceFactory(resourceBase); - if (resourceBaseUrls.isEmpty()) { - return Collections.singletonList(new StaticServlet(null)); - } + String pathInfoOnly = resolvePathInfoOnly(); + Collection resourceBases = resolveResourceBases(); // "classpath:" URLs can point to multiple locations. Map them to multiple delegated servlets - List delegates = new ArrayList<>(resourceBaseUrls.size()); - for (URL baseUrl : resourceBaseUrls) { - delegates.add(new StaticServlet(baseUrl.toExternalForm())); + List delegates = new ArrayList<>(resourceBases.size()); + for (URL baseUrl : resourceBases) { + delegates.add(new StaticServlet(baseUrl.toExternalForm(), pathInfoOnly)); } - if(delegates.size() > 1) { - LOGGER.info("Found multiple base URLs for resource base '{}': {}", resourceBase, resourceBaseUrls); + if (delegates.isEmpty()) { + return Collections.singletonList(new StaticServlet(null, pathInfoOnly)); + } else if (delegates.size() > 1) { + LOGGER.info("Found multiple base URLs for resource base '{}': {}", resourceBase, resourceBases); } return delegates; } - protected Collection resolveFolderResourceFactory(String path) { + protected Collection resolveResourceBases() { + FolderResourceFactory resourceBase = resolveResourceBase(); try { - return new FolderResourceFactory(path).getUrls(); + return resourceBase != null ? resourceBase.getUrls() : Collections.emptyList(); } catch (IllegalArgumentException e) { + // log, but allow to start - LOGGER.warn("Static servlet base directory '{}' does not exist", path); + // TODO: why are we so lenient here, should we throw? + + LOGGER.warn("Static servlet resource base folder '{}' does not exist", resourceBase.getResourceId()); return Collections.emptyList(); } } + protected FolderResourceFactory resolveResourceBase() { + // this.resourceBase is allowed to be null; also it can be overridden by the servlet parameter + String paramResourceBase = getInitParameter(StaticServlet.RESOURCE_BASE_PARAMETER); + return paramResourceBase != null ? new FolderResourceFactory(paramResourceBase) : this.resourceBase; + } + + protected String resolvePathInfoOnly() { + String paramValue = getInitParameter(StaticServlet.PATH_INFO_ONLY_PARAMETER); + return paramValue != null ? paramValue : this.pathInfoOnly; + } + @FunctionalInterface interface DoGetProcessor { void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; @@ -128,7 +151,7 @@ interface DoGetProcessor { static class DoGetOne implements DoGetProcessor { - private StaticServlet delegate; + private final StaticServlet delegate; DoGetOne(StaticServlet delegate) { this.delegate = delegate; @@ -142,7 +165,7 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws Servl static class DoGetMany implements DoGetProcessor { - private StaticServlet[] delegates; + private final StaticServlet[] delegates; public DoGetMany(StaticServlet[] delegates) { this.delegates = delegates; diff --git a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/StaticServlet.java b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/StaticServlet.java index e6933bf9..f73120c7 100644 --- a/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/StaticServlet.java +++ b/bootique-jetty-jakarta/src/main/java/io/bootique/jetty/servlet/StaticServlet.java @@ -30,18 +30,41 @@ */ public class StaticServlet extends DefaultServlet { + static final String PATH_INFO_ONLY_PARAMETER = "pathInfoOnly"; static final String RESOURCE_BASE_PARAMETER = "resourceBase"; private final String resourceBase; + // capturing this as a String instead of boolean to allow Jetty apply its own string to boolean parsing + private final String pathInfoOnly; + /** + * @deprecated in favor of {@link #StaticServlet(String, String)} + */ + @Deprecated(since = "3.0", forRemoval = true) public StaticServlet(String resourceBase) { + this(resourceBase, "false"); + } + + /** + * @since 3.0 + */ + public StaticServlet(String resourceBase, String pathInfoOnly) { this.resourceBase = resourceBase; + this.pathInfoOnly = pathInfoOnly; } @Override public String getInitParameter(String name) { - // ignore super value if the parameter is "resourceBase" - return RESOURCE_BASE_PARAMETER.equals(name) ? this.resourceBase : super.getInitParameter(name); + + // special rules for Bootique-defined parameters + switch (name) { + case PATH_INFO_ONLY_PARAMETER: + return this.pathInfoOnly; + case RESOURCE_BASE_PARAMETER: + return this.resourceBase; + default: + return super.getInitParameter(name); + } } // making public, so we can call it from MultiBaseDefaultServlet diff --git a/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/StaticServletIT.java b/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/StaticServletIT.java index 7ac26396..5c6ad044 100644 --- a/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/StaticServletIT.java +++ b/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/StaticServletIT.java @@ -38,7 +38,8 @@ public class StaticServletIT { final BQTestFactory testFactory = new BQTestFactory().autoLoadModules(); @Test - public void commonResourceBase() { + @Deprecated + public void commonResourceBase_addStaticServlet() { testFactory.app("-s") .module(b -> { @@ -68,7 +69,39 @@ public void commonResourceBase() { } @Test - public void resourcePathResolving() { + public void commonResourceBase() { + + testFactory.app("-s") + .module(b -> { + MappedServlet s = MappedServlet.ofStatic("sub").urlPatterns("/sub1/*", "/sub2/*").build(); + JettyModule.extend(b).addMappedServlet(s); + BQCoreModule.extend(b).setProperty("bq.jetty.staticResourceBase", + "src/test/resources/io/bootique/jetty/StaticResourcesIT_docroot_subfolders/"); + }) + .run(); + + WebTarget base = ClientBuilder.newClient().target("http://localhost:8080"); + + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), base.path("/").request().get().getStatus()); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), base.path("/other.txt").request().get().getStatus()); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), base.path("/sub3/other.txt").request().get().getStatus()); + + Response r1 = base.path("/sub1/other.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("other1", r1.readEntity(String.class)); + + Response r2 = base.path("/sub2/other.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("other2", r2.readEntity(String.class)); + + Response r3 = base.path("/sub2/").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r3.getStatus()); + assertEquals("

2

", r3.readEntity(String.class)); + } + + @Test + @Deprecated + public void resourcePathResolving_addStaticServlet() { testFactory.app("-s") .module(b -> { @@ -123,6 +156,113 @@ public void resourcePathResolving() { assertEquals("[/f.txt]", r4.readEntity(String.class)); } + @Test + public void builderParams() { + + testFactory.app("-s") + .module(b -> { + + BQCoreModule.extend(b) + // shared resource base; some servlets use it implicitly, others override it + .setProperty("bq.jetty.staticResourceBase", + "src/test/resources/io/bootique/jetty/ResourcePathResolving/"); + + JettyModule.extend(b) + // s1: own resource base and "pathInfoOnly == false" (so servlet path is a part of the + // static folder path) + .addMappedServlet(MappedServlet.ofStatic("s1").urlPatterns("/sub1/*").resourceBase("classpath:io/bootique/jetty/ResourcePathResolving/root1/").build()) + + // s2: own resource base and "pathInfoOnly == true" (so servlet path is excluded from the + // static folder path) + .addMappedServlet(MappedServlet.ofStatic("s2").urlPatterns("/sub2/*").resourceBase("classpath:io/bootique/jetty/ResourcePathResolving/root2/").pathInfoOnly().build()) + + // s3: shared resource base and "pathInfoOnly == false" + .addMappedServlet(MappedServlet.ofStatic("s3").urlPatterns("/sub3/*").build()) + + // s4: shared resource base and "pathInfoOnly == true" + .addMappedServlet(MappedServlet.ofStatic("s4").urlPatterns("/sub4/*").pathInfoOnly().build()); + }) + .run(); + + WebTarget base = ClientBuilder.newClient().target("http://localhost:8080"); + + Response r1 = base.path("/sub1/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("[/root1/sub1/f.txt]", r1.readEntity(String.class)); + + Response r2 = base.path("/sub2/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("[/root2/f.txt]", r2.readEntity(String.class)); + + Response r3 = base.path("/sub3/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r3.getStatus()); + assertEquals("[/sub3/f.txt]", r3.readEntity(String.class)); + + Response r4 = base.path("/sub4/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r4.getStatus()); + assertEquals("[/f.txt]", r4.readEntity(String.class)); + } + + @Test + public void overrideFromParams() { + + testFactory.app("-s") + .module(b -> { + JettyModule.extend(b) + // this "pathInfo" should be overridden by params "pathInfo" of "false" + .addMappedServlet(MappedServlet.ofStatic("s1").urlPatterns("/sub1/*").pathInfoOnly().build()) + + // this resource base should be overridden by params resource base + .addMappedServlet(MappedServlet.ofStatic("s2").urlPatterns("/sub2/*").resourceBase("classpath:io/bootique/jetty/ResourcePathResolving/root1/").build()) + .addMappedServlet(MappedServlet.ofStatic("s3").urlPatterns("/sub3/*").build()) + .addMappedServlet(MappedServlet.ofStatic("s4").urlPatterns("/sub4/*").build()); + + BQCoreModule.extend(b) + + // shared resource base; some servlets use it implicitly, others override it + .setProperty("bq.jetty.staticResourceBase", + "src/test/resources/io/bootique/jetty/ResourcePathResolving/") + + // s1: own resource base and "pathInfoOnly == false" (so servlet path is a part of the + // static folder path) + .setProperty("bq.jetty.servlets.s1.params.resourceBase", + "src/test/resources/io/bootique/jetty/ResourcePathResolving/root1/") + .setProperty("bq.jetty.servlets.s1.params.pathInfoOnly", "false") + + // s2: own resource base and "pathInfoOnly == true" (so servlet path is excluded from the + // static folder path) + .setProperty("bq.jetty.servlets.s2.params.resourceBase", + "src/test/resources/io/bootique/jetty/ResourcePathResolving/root2/") + .setProperty("bq.jetty.servlets.s2.params.pathInfoOnly", "true") + + // s3: shared resource base and "pathInfoOnly == false" + // ... + + // s4: shared resource base and "pathInfoOnly == true" + .setProperty("bq.jetty.servlets.s4.params.pathInfoOnly", "true"); + + }) + .run(); + + WebTarget base = ClientBuilder.newClient().target("http://localhost:8080"); + + Response r1 = base.path("/sub1/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("[/root1/sub1/f.txt]", r1.readEntity(String.class)); + + Response r2 = base.path("/sub2/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("[/root2/f.txt]", r2.readEntity(String.class)); + + Response r3 = base.path("/sub3/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r3.getStatus()); + assertEquals("[/sub3/f.txt]", r3.readEntity(String.class)); + + Response r4 = base.path("/sub4/f.txt").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r4.getStatus()); + assertEquals("[/f.txt]", r4.readEntity(String.class)); + } + @Test public void resourceBaseClasspath_Missing() { diff --git a/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/AnnotatedFilterIT.java b/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/AnnotatedFilterIT.java index 2a6c37e7..2dd305fd 100644 --- a/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/AnnotatedFilterIT.java +++ b/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/AnnotatedFilterIT.java @@ -33,7 +33,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -119,8 +118,7 @@ public void destroy() { } @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { assertion = () -> { }; } diff --git a/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/ServletEnvironmentIT.java b/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/ServletEnvironmentIT.java index 76ac7a6b..e3d41e80 100644 --- a/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/ServletEnvironmentIT.java +++ b/bootique-jetty-jakarta/src/test/java/io/bootique/jetty/servlet/ServletEnvironmentIT.java @@ -84,7 +84,7 @@ class ServletCheckingModule implements BQModule { @Override public void configure(Binder binder) { - TypeLiteral> st = new TypeLiteral>() {}; + TypeLiteral> st = new TypeLiteral<>() {}; JettyModule.extend(binder).addMappedServlet(st); }