Skip to content

Commit

Permalink
Unified API for mapping static servlets #125
Browse files Browse the repository at this point in the history
  • Loading branch information
andrus committed May 25, 2024
1 parent c46962b commit 91d2840
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 85 deletions.
4 changes: 4 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -104,40 +103,42 @@ public JettyModuleExtender addListener(Class<? extends EventListener> listenerTy
}

/**
* @param mappedListener
* @param <T>
* @return
* @return this extender instance
*/
public <T extends EventListener> JettyModuleExtender addMappedListener(MappedListener<T> mappedListener) {
contributeMappedListeners().addInstance(mappedListener);
return this;
}

/**
* @param mappedListenerKey
* @param <T>
* @return
* @return this extender instance
*/
public <T extends EventListener> JettyModuleExtender addMappedListener(Key<MappedListener<T>> mappedListenerKey) {
contributeMappedListeners().add(mappedListenerKey);
return this;
}

/**
* @param mappedListenerType
* @param <T>
* @return this extender instance
*/
public <T extends EventListener> JettyModuleExtender addMappedListener(TypeLiteral<MappedListener<T>> 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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T extends Servlet> extends MappedWebArtifact<T> {

public MappedServlet(T servlet, Set<String> 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<String> 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<String> urlPatterns, String name, Map<String, String> 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<String> 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<String> 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<String> urlPatterns, String name, Map<String, String> 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<String> patterns = this.urlPatterns != null && this.urlPatterns.length > 0
? Set.of(this.urlPatterns)
: Set.of("/");

return new MappedServlet<>(servlet, patterns, name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<StaticServlet> 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
Expand Down Expand Up @@ -88,47 +100,58 @@ protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws
}

protected List<StaticServlet> createDelegates() {
String resourceBase = getInitParameter(StaticServlet.RESOURCE_BASE_PARAMETER);
if (resourceBase == null) {
return Collections.singletonList(new StaticServlet(null));
}

Collection<URL> resourceBaseUrls = resolveFolderResourceFactory(resourceBase);
if (resourceBaseUrls.isEmpty()) {
return Collections.singletonList(new StaticServlet(null));
}
String pathInfoOnly = resolvePathInfoOnly();
Collection<URL> resourceBases = resolveResourceBases();

// "classpath:" URLs can point to multiple locations. Map them to multiple delegated servlets
List<StaticServlet> delegates = new ArrayList<>(resourceBaseUrls.size());
for (URL baseUrl : resourceBaseUrls) {
delegates.add(new StaticServlet(baseUrl.toExternalForm()));
List<StaticServlet> 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<URL> resolveFolderResourceFactory(String path) {
protected Collection<URL> 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;
}

static class DoGetOne implements DoGetProcessor {

private StaticServlet delegate;
private final StaticServlet delegate;

DoGetOne(StaticServlet delegate) {
this.delegate = delegate;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 91d2840

Please sign in to comment.