Jetty 12.0.x resource Servlet (#11933)

Introduce ResourceServlet
* Split DefaultServlet into ResourceServlet
* Added tests for Resource and Default Servlet
* Improved documentation of the ResourceServlet
* Fixed the documentation
This commit is contained in:
Greg Wilkins 2024-07-10 09:05:56 +10:00 committed by GitHub
parent 29d27f7373
commit 46204a86ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 5005 additions and 900 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>org.eclipse.jetty.documentation</groupId>
<artifactId>documentation</artifactId>
<version>12.0.10-SNAPSHOT</version>
<version>12.0.12-SNAPSHOT</version>
</parent>
<artifactId>jetty-asciidoctor-extensions</artifactId>
<packaging>jar</packaging>

View File

@ -5,10 +5,10 @@
<parent>
<groupId>org.eclipse.jetty.documentation</groupId>
<artifactId>documentation</artifactId>
<version>12.0.10-SNAPSHOT</version>
<version>12.0.12-SNAPSHOT</version>
</parent>
<artifactId>jetty-documentation</artifactId>
<packaging>pom</packaging>
<packaging>jar</packaging>
<name>Documentation :: Guides</name>
<properties>
@ -49,15 +49,20 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-home</artifactId>
<version>${project.version}</version>
<type>zip</type>
<optional>true</optional>
<artifactId>jetty-infinispan-common</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-infinispan-embedded-query</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-infinispan-remote-query</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>

View File

@ -654,3 +654,10 @@ Below you can find an example of how to setup `DefaultServlet`:
----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultServlet]
----
If you wish to serve resources from a non-default location, then the `ResourceServlet` should be used instead of the `DefaultServlet`. Below you can find an example of how to setup both a `DefaultServlet` and a `ResourceServlet`:
[source,java,indent=0]
----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=resourceServlet]
----

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
import org.eclipse.jetty.ee10.servlet.ResourceServlet;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
@ -1220,11 +1221,32 @@ public class HTTPServerDocs
// Add the DefaultServlet to serve static content.
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
// Configure the DefaultServlet with init-parameters.
servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/");
servletHolder.setInitParameter("maxCacheSize", "8388608");
servletHolder.setInitParameter("dirAllowed", "true");
servletHolder.setAsyncSupported(true);
// end::defaultServlet[]
}
public void resourceServlet()
{
// tag::resourceServlet[]
// Create a ServletContextHandler with contextPath.
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/app");
// Add the ResourceServlet to serve static content from a specific location.
ServletHolder servletHolder = context.addServlet(ResourceServlet.class, "/static/*");
// Configure the ResourceServlet with init-parameters.
servletHolder.setInitParameter("baseResource", "/absolute/path/to/static/resources/");
servletHolder.setInitParameter("pathInfoOnly", "true");
servletHolder.setAsyncSupported(true);
// Add the DefaultServlet to serve static content from the resource base
ServletHolder defaultHolder = context.addServlet(DefaultServlet.class, "/");
defaultHolder.setAsyncSupported(true);
// end::resourceServlet[]
}
public void serverGzipHandler() throws Exception
{
// tag::serverGzipHandler[]

View File

@ -1220,7 +1220,7 @@ public class HTTPServerDocs
// Add the DefaultServlet to serve static content.
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
// Configure the DefaultServlet with init-parameters.
servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/");
servletHolder.setInitParameter("baseResource", "/path/to/static/resources/");
servletHolder.setAsyncSupported(true);
// end::defaultServlet[]
}

View File

@ -14,5 +14,7 @@
<modules>
<module>jetty</module>
<module>jetty-asciidoctor-extensions</module>
<module>jetty-documentation</module>
</modules>
</project>

View File

@ -919,6 +919,12 @@ public interface Request extends Attributes, Content.Source
{
return _request;
}
@Override
public String toString()
{
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getWrapped());
}
}
@SuppressWarnings("unchecked")

View File

@ -13,374 +13,36 @@
package org.eclipse.jetty.ee10.servlet;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.MappingMatch;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.content.FileMappingHttpContentFactory;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory;
import org.eclipse.jetty.http.content.VirtualHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.ResourceService;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.util.URIUtil.encodePath;
/**
* <p>The default Servlet, normally mapped to {@code /}, that handles static resources.</p>
* <p>The following init parameters are supported:</p>
* <dl>
* <dt>acceptRanges</dt>
* <dd>
* Use {@code true} to accept range requests, defaults to {@code true}.
* </dd>
* <dt>baseResource</dt>
* <dd>
* Defaults to the context's baseResource.
* The root directory to look for static resources.
* </dd>
* <dt>cacheControl</dt>
* <dd>
* The value of the {@code Cache-Control} header.
* If omitted, no {@code Cache-Control} header is generated in responses.
* By default is omitted.
* </dd>
* <dt>cacheValidationTime</dt>
* <dd>
* How long in milliseconds a resource is cached.
* If omitted, defaults to {@code 1000} ms.
* Use {@code -1} to cache forever or {@code 0} to not cache.
* </dd>
* <dt>dirAllowed</dt>
* <dd>
* Use {@code true} to serve directory listing if no welcome file is found.
* Otherwise responds with {@code 403 Forbidden}.
* Defaults to {@code true}.
* </dd>
* <dt>encodingHeaderCacheSize</dt>
* <dd>
* Max number of cached {@code Accept-Encoding} entries.
* Use {@code -1} for the default value (100), {@code 0} for no cache.
* </dd>
* <dt>etags</dt>
* <dd>
* Use {@code true} to generate ETags in responses.
* Defaults to {@code false}.
* </dd>
* <dt>maxCachedFiles</dt>
* <dd>
* The max number of cached static resources.
* Use {@code -1} for the default value (2048) or {@code 0} for no cache.
* </dd>
* <dt>maxCachedFileSize</dt>
* <dd>
* The max size in bytes of a single cached static resource.
* Use {@code -1} for the default value (128 MiB) or {@code 0} for no cache.
* </dd>
* <dt>maxCacheSize</dt>
* <dd>
* The max size in bytes of the cache for static resources.
* Use {@code -1} for the default value (256 MiB) or {@code 0} for no cache.
* </dd>
* <dt>otherGzipFileExtensions</dt>
* <dd>
* A comma-separated list of extensions of files whose content is implicitly
* gzipped.
* Defaults to {@code .svgz}.
* </dd>
* <dt>precompressed</dt>
* <dd>
* Omitted by default, so that no pre-compressed content will be served.
* If set to {@code true}, the default set of pre-compressed formats will be used.
* Otherwise can be set to a comma-separated list of {@code encoding=extension} pairs,
* such as: {@code br=.br,gzip=.gz,bzip2=.bz}, where {@code encoding} is used as the
* value for the {@code Content-Encoding} header.
* </dd>
* <dt>redirectWelcome</dt>
* <dd>
* Use {@code true} to redirect welcome files, otherwise they are forwarded.
* Defaults to {@code false}.
* </dd>
* <dt>stylesheet</dt>
* <dd>
* Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
* The path of a custom stylesheet to style the directory listing HTML.
* </dd>
* <dt>useFileMappedBuffer</dt>
* <dd>
* Use {@code true} to use file mapping to serve static resources.
* Defaults to {@code false}.
* </dd>
* <dt>welcomeServlets</dt>
* <dd>
* Use {@code false} to only serve welcome resources from the file system.
* Use {@code true} to dispatch welcome resources to a matching Servlet
* (for example mapped to {@code *.welcome}), when the welcome resources
* does not exist on file system.
* Use {@code exact} to dispatch welcome resource to a Servlet whose mapping
* is exactly the same as the welcome resource (for example {@code /index.welcome}),
* when the welcome resources does not exist on file system.
* Defaults to {@code false}.
* </dd>
* </dl>
* <p>The {@code DefaultServlet}, is a specialization of the {@link ResourceServlet} to be mapped to {@code /} as the "default"
* servlet for a context.
* </p>
* <p>
* In addition to the servlet init parameters that can be used to configure any {@link ResourceServlet}, the DefaultServlet
* also looks at {@link ServletContext#getInitParameter(String)} for any parameter starting with {@link #CONTEXT_INIT}, which
* is then stripped and the resulting name interpreted as a {@link ResourceServlet} init parameter.
* </p>
* <p>
* To serve static content other than as the {@code DefaultServlet} mapped to "/", please use the {@link ResourceServlet} directly.
* The {@code DefaultServlet} will warn if it is used other than as the default servlet. In future, this may become a fatal error.
* </p>
*/
public class DefaultServlet extends HttpServlet
public class DefaultServlet extends ResourceServlet
{
private static final Logger LOG = LoggerFactory.getLogger(DefaultServlet.class);
public static final String CONTEXT_INIT = "org.eclipse.jetty.servlet.Default.";
private ServletContextHandler _contextHandler;
private ServletResourceService _resourceService;
private WelcomeServletMode _welcomeServletMode;
public ResourceService getResourceService()
{
return _resourceService;
}
@Override
public void init() throws ServletException
{
_contextHandler = initContextHandler(getServletContext());
_resourceService = new ServletResourceService(_contextHandler);
_resourceService.setWelcomeFactory(_resourceService);
Resource baseResource = _contextHandler.getBaseResource();
String rb = getInitParameter("baseResource", "resourceBase");
if (rb != null)
{
try
{
baseResource = URIUtil.isRelative(rb) ? _contextHandler.getBaseResource().resolve(rb) : _contextHandler.newResource(rb);
}
catch (Exception e)
{
LOG.warn("Unable to create baseResource from {}", rb, e);
throw new UnavailableException(e.toString());
}
}
if (baseResource != null && !(baseResource.isDirectory() && baseResource.isReadable()))
LOG.warn("baseResource {} is not a readable directory", baseResource);
List<CompressedContentFormat> precompressedFormats = parsePrecompressedFormats(getInitParameter("precompressed"),
getInitBoolean("gzip"), _resourceService.getPrecompressedFormats());
// Try to get factory from ServletContext attribute.
HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName());
if (contentFactory == null)
{
MimeTypes mimeTypes = _contextHandler.getMimeTypes();
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes);
// Use the servers default stylesheet unless there is one explicitly set by an init param.
Resource styleSheet = _contextHandler.getServer().getDefaultStyleSheet();
String stylesheetParam = getInitParameter("stylesheet");
if (stylesheetParam != null)
{
try
{
HttpContent styleSheetContent = contentFactory.getContent(stylesheetParam);
Resource s = styleSheetContent == null ? null : styleSheetContent.getResource();
if (Resources.isReadableFile(s))
styleSheet = s;
else
LOG.warn("Stylesheet {} does not exist", stylesheetParam);
}
catch (Exception e)
{
if (LOG.isDebugEnabled())
LOG.warn("Unable to use stylesheet: {}", stylesheetParam, e);
else
LOG.warn("Unable to use stylesheet: {} - {}", stylesheetParam, e.toString());
}
}
if (getInitBoolean("useFileMappedBuffer", false))
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css");
contentFactory = new PreCompressedHttpContentFactory(contentFactory, precompressedFormats);
int maxCacheSize = getInitInt("maxCacheSize", -2);
int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
int maxCachedFiles = getInitInt("maxCachedFiles", -2);
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{
ByteBufferPool bufferPool = getByteBufferPool(_contextHandler);
ValidatingCachingHttpContentFactory cached = new ValidatingCachingHttpContentFactory(contentFactory,
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
contentFactory = cached;
if (maxCacheSize >= 0)
cached.setMaxCacheSize(maxCacheSize);
if (maxCachedFileSize >= 0)
cached.setMaxCachedFileSize(maxCachedFileSize);
if (maxCachedFiles >= 0)
cached.setMaxCachedFiles(maxCachedFiles);
}
}
_resourceService.setHttpContentFactory(contentFactory);
if (_contextHandler.getWelcomeFiles() == null)
_contextHandler.setWelcomeFiles(new String[]{"index.html", "index.jsp"});
_resourceService.setAcceptRanges(getInitBoolean("acceptRanges", _resourceService.isAcceptRanges()));
_resourceService.setDirAllowed(getInitBoolean("dirAllowed", _resourceService.isDirAllowed()));
boolean redirectWelcome = getInitBoolean("redirectWelcome", false);
_resourceService.setWelcomeMode(redirectWelcome ? ResourceService.WelcomeMode.REDIRECT : ResourceService.WelcomeMode.SERVE);
_resourceService.setPrecompressedFormats(precompressedFormats);
_resourceService.setEtags(getInitBoolean("etags", _resourceService.isEtags()));
_welcomeServletMode = WelcomeServletMode.NONE;
String welcomeServlets = getInitParameter("welcomeServlets");
if (welcomeServlets != null)
{
welcomeServlets = welcomeServlets.toLowerCase(Locale.ENGLISH);
_welcomeServletMode = switch (welcomeServlets)
{
case "true" -> WelcomeServletMode.MATCH;
case "exact" -> WelcomeServletMode.EXACT;
default -> WelcomeServletMode.NONE;
};
}
int encodingHeaderCacheSize = getInitInt("encodingHeaderCacheSize", -1);
if (encodingHeaderCacheSize >= 0)
_resourceService.setEncodingCacheSize(encodingHeaderCacheSize);
String cc = getInitParameter("cacheControl");
if (cc != null)
_resourceService.setCacheControl(cc);
List<String> gzipEquivalentFileExtensions = new ArrayList<>();
String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
if (otherGzipExtensions != null)
{
//comma separated list
StringTokenizer tok = new StringTokenizer(otherGzipExtensions, ",", false);
while (tok.hasMoreTokens())
{
String s = tok.nextToken().trim();
gzipEquivalentFileExtensions.add((s.charAt(0) == '.' ? s : "." + s));
}
}
else
{
// .svgz files are gzipped svg files and must be served with Content-Encoding:gzip
gzipEquivalentFileExtensions.add(".svgz");
}
_resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions);
if (LOG.isDebugEnabled())
{
LOG.debug(" .baseResource = {}", baseResource);
LOG.debug(" .resourceService = {}", _resourceService);
LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode);
}
}
private static ByteBufferPool getByteBufferPool(ContextHandler contextHandler)
{
if (contextHandler == null)
return ByteBufferPool.NON_POOLING;
Server server = contextHandler.getServer();
if (server == null)
return ByteBufferPool.NON_POOLING;
return server.getByteBufferPool();
}
private String getInitParameter(String name, String... deprecated)
{
String value = getInitParameter(name);
if (value != null)
return value;
for (String d : deprecated)
{
value = getInitParameter(d);
if (value != null)
{
LOG.warn("Deprecated {} used instead of {}", d, name);
return value;
}
}
return null;
}
private List<CompressedContentFormat> parsePrecompressedFormats(String precompressed, Boolean gzip, List<CompressedContentFormat> dft)
{
if (precompressed == null && gzip == null)
{
return dft;
}
List<CompressedContentFormat> ret = new ArrayList<>();
if (precompressed != null && precompressed.indexOf('=') > 0)
{
for (String pair : precompressed.split(","))
{
String[] setting = pair.split("=");
String encoding = setting[0].trim();
String extension = setting[1].trim();
ret.add(new CompressedContentFormat(encoding, extension));
if (gzip == Boolean.TRUE && !ret.contains(CompressedContentFormat.GZIP))
ret.add(CompressedContentFormat.GZIP);
}
}
else if (precompressed != null)
{
if (Boolean.parseBoolean(precompressed))
{
ret.add(CompressedContentFormat.BR);
ret.add(CompressedContentFormat.GZIP);
}
}
else if (gzip == Boolean.TRUE)
{
// gzip handling is for backwards compatibility with older Jetty
ret.add(CompressedContentFormat.GZIP);
}
return ret;
}
private final AtomicBoolean warned = new AtomicBoolean(false);
/**
* <p>
@ -405,519 +67,55 @@ public class DefaultServlet extends HttpServlet
return value;
}
private Boolean getInitBoolean(String name)
{
String value = getInitParameter(name);
if (value == null || value.length() == 0)
return null;
return (value.startsWith("t") ||
value.startsWith("T") ||
value.startsWith("y") ||
value.startsWith("Y") ||
value.startsWith("1"));
}
private boolean getInitBoolean(String name, boolean dft)
{
return Optional.ofNullable(getInitBoolean(name)).orElse(dft);
}
private int getInitInt(String name, int dft)
{
String value = getInitParameter(name);
if (value != null && value.length() > 0)
return Integer.parseInt(value);
return dft;
}
protected ServletContextHandler initContextHandler(ServletContext servletContext)
{
if (servletContext instanceof ServletContextHandler.ServletContextApi api)
return api.getContext().getServletContextHandler();
Context context = ContextHandler.getCurrentContext();
if (context instanceof ContextHandler.ScopedContext scopedContext)
return scopedContext.getContextHandler();
throw new IllegalArgumentException("The servletContext " + servletContext + " " +
servletContext.getClass().getName() + " is not " + ContextHandler.ScopedContext.class.getName());
}
@Override
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
public void init() throws ServletException
{
String includedServletPath = (String)httpServletRequest.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
String encodedPathInContext = getEncodedPathInContext(httpServletRequest, includedServletPath);
boolean included = includedServletPath != null;
if (LOG.isDebugEnabled())
LOG.debug("doGet(hsReq={}, hsResp={}) pathInContext={}, included={}", httpServletRequest, httpServletResponse, encodedPathInContext, included);
try
{
HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(httpServletRequest));
if (LOG.isDebugEnabled())
LOG.debug("content = {}", content);
if (content == null || Resources.missing(content.getResource()))
{
if (included)
{
/* https://github.com/jakartaee/servlet/blob/6.0.0-RELEASE/spec/src/main/asciidoc/servlet-spec-body.adoc#93-the-include-method
* 9.3 - If the default servlet is the target of a RequestDispatch.include() and the requested
* resource does not exist, then the default servlet MUST throw FileNotFoundException.
* If the exception isnt caught and handled, and the response
* hasnt been committed, the status code MUST be set to 500.
*/
throw new FileNotFoundException(encodedPathInContext);
}
// no content
httpServletResponse.sendError(404);
}
else
{
// lookup the core request and response as wrapped by the ServletContextHandler
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest);
ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse();
ServletChannel servletChannel = servletContextRequest.getServletChannel();
// If the servlet request has not been wrapped,
// we can use the core request directly,
// otherwise wrap the servlet request as a core request
Request coreRequest = httpServletRequest instanceof ServletApiRequest
? servletChannel.getRequest()
: ServletCoreRequest.wrap(httpServletRequest);
// If the servlet response has been wrapped and has been written to,
// then the servlet response must be wrapped as a core response
// otherwise we can use the core response directly.
boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming();
Response coreResponse = useServletResponse
? new ServletCoreResponse(coreRequest, httpServletResponse, included)
: servletChannel.getResponse();
// If the core response is already committed then do nothing more
if (coreResponse.isCommitted())
{
if (LOG.isDebugEnabled())
LOG.debug("Response already committed for {}", coreRequest.getHttpURI());
return;
}
// Get the content length before we may wrap the content
long contentLength = content.getContentLengthValue();
// Servlet Filters could be interacting with the Response already.
if (useServletResponse)
content = new UnknownLengthHttpContent(content);
// The character encoding may be forced
String characterEncoding = servletContextResponse.getRawCharacterEncoding();
if (characterEncoding != null)
content = new ForcedCharacterEncodingHttpContent(content, characterEncoding);
// If async is supported and the unwrapped content is larger than an output buffer
if (httpServletRequest.isAsyncSupported() &&
(contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()))
{
// send the content asynchronously
AsyncContext asyncContext = httpServletRequest.startAsync();
Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse);
_resourceService.doGet(coreRequest, coreResponse, callback, content);
}
else
{
// send the content blocking
try (Blocker.Callback callback = Blocker.callback())
{
_resourceService.doGet(coreRequest, coreResponse, callback, content);
callback.block();
}
catch (Exception e)
{
throw new ServletException(e);
}
}
}
}
catch (InvalidPathException e)
{
if (LOG.isDebugEnabled())
LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
if (included)
throw new FileNotFoundException(encodedPathInContext);
httpServletResponse.setStatus(404);
}
if ("true".equalsIgnoreCase(getInitParameter("pathInfoOnly")))
LOG.warn("DefaultServlet pathInfoOnly is set to true. Use ResourceServlet instead.");
super.init();
}
/**
* Get the path in the context, of the resource to serve for a request.
* The default implementation considers the {@link jakarta.servlet.http.HttpServletMapping} of the request and
* any {@link Dispatcher#INCLUDE_SERVLET_PATH} attributes that may be set.
* @param request The request
* @param included {@code true} if the request is for an included resource
* @return The encoded URI path of the resource to server, relative to the resource base.
*/
@Override
protected String getEncodedPathInContext(HttpServletRequest request, boolean included)
{
String deprecatedPath = getEncodedPathInContext(request, (String)(included ? request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH) : null));
if (deprecatedPath != null)
return deprecatedPath;
if (request.getHttpServletMapping().getMappingMatch() != MappingMatch.DEFAULT)
{
if (warned.compareAndSet(false, true))
LOG.warn("Incorrect mapping for DefaultServlet at %s. Use ResourceServlet".formatted(request.getHttpServletMapping().getPattern()));
return super.getEncodedPathInContext(request, included);
}
if (included)
{
if (request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH) instanceof String servletPath)
return URIUtil.encodePath(servletPath);
// must be an include of a named dispatcher. Just use the whole URI
return URIUtil.encodePath(request.getServletPath());
}
if (request instanceof ServletApiRequest apiRequest)
// Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
return Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
return URIUtil.encodePath(request.getServletPath());
}
@Deprecated(forRemoval = true)
protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath)
{
if (includedServletPath != null)
return encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req)));
else if (!isDefaultMapping(req))
{
//a match via an extension mapping will more than likely
//have no path info
String path = req.getPathInfo();
if (StringUtil.isEmpty(path) &&
MappingMatch.EXTENSION.equals(req.getHttpServletMapping().getMappingMatch()))
path = req.getServletPath();
return encodePath(path);
}
else if (req instanceof ServletApiRequest apiRequest)
return Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
else
return Context.getPathInContext(req.getContextPath(), URIUtil.canonicalPath(req.getRequestURI()));
}
@Override
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
if (LOG.isDebugEnabled())
LOG.debug("doHead(req={}, resp={}) (calling doGet())", req, resp);
doGet(req, resp);
}
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
// Always return 405: Method Not Allowed for DefaultServlet
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
// override to eliminate TRACE that the default HttpServlet impl adds
resp.setHeader("Allow", "GET, HEAD, OPTIONS");
}
private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
{
private final ServletContextHandler _servletContextHandler;
private ServletResourceService(ServletContextHandler servletContextHandler)
{
_servletContextHandler = servletContextHandler;
}
@Override
public String getWelcomeTarget(HttpContent content, Request coreRequest)
{
String[] welcomes = _servletContextHandler.getWelcomeFiles();
if (welcomes == null)
return null;
String pathInContext = Request.getPathInContext(coreRequest);
String welcomeTarget = null;
Resource base = content.getResource();
if (Resources.isReadableDirectory(base))
{
for (String welcome : welcomes)
{
String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
// If the welcome resource is a file, it has
// precedence over resources served by Servlets.
Resource welcomePath = content.getResource().resolve(welcome);
if (Resources.isReadableFile(welcomePath))
return welcomeInContext;
// Check whether a Servlet may serve the welcome resource.
if (_welcomeServletMode != WelcomeServletMode.NONE && welcomeTarget == null)
{
if (!isDefaultMapping(getServletRequest(coreRequest)) && !isIncluded(getServletRequest(coreRequest)))
welcomeTarget = URIUtil.addPaths(getServletRequest(coreRequest).getPathInfo(), welcome);
ServletHandler.MappedServlet entry = _servletContextHandler.getServletHandler().getMappedServlet(welcomeInContext);
// Is there a different Servlet that may serve the welcome resource?
if (entry != null && entry.getServletHolder().getServletInstance() != DefaultServlet.this)
{
if (_welcomeServletMode == WelcomeServletMode.MATCH || entry.getPathSpec().getDeclaration().equals(welcomeInContext))
{
welcomeTarget = welcomeInContext;
// Do not break the loop, because we want to try other welcome resources
// that may be files and take precedence over Servlet welcome resources.
}
}
}
}
}
return welcomeTarget;
}
@Override
protected void serveWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
{
HttpServletRequest servletRequest = getServletRequest(request);
HttpServletResponse servletResponse = getServletResponse(response);
boolean included = isIncluded(servletRequest);
RequestDispatcher dispatcher = servletRequest.getServletContext().getRequestDispatcher(welcomeTarget);
if (dispatcher == null)
{
// We know that the welcome target exists and can be served.
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}
try
{
if (included)
{
dispatcher.include(servletRequest, servletResponse);
}
else
{
servletRequest.setAttribute("org.eclipse.jetty.server.welcome", welcomeTarget);
dispatcher.forward(servletRequest, servletResponse);
}
callback.succeeded();
}
catch (ServletException e)
{
callback.failed(e);
}
}
@Override
protected void rehandleWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
{
serveWelcome(request, response, callback, welcomeTarget);
}
@Override
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode)
{
if (LOG.isDebugEnabled())
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={})", coreRequest, coreResponse, callback, statusCode);
writeHttpError(coreRequest, coreResponse, callback, statusCode, null, null);
}
@Override
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, Throwable cause)
{
if (LOG.isDebugEnabled())
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, cause={})", coreRequest, coreResponse, callback, cause, cause);
int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500;
String reason = null;
if (cause instanceof HttpException httpException)
{
statusCode = httpException.getCode();
reason = httpException.getReason();
}
writeHttpError(coreRequest, coreResponse, callback, statusCode, reason, cause);
}
@Override
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode, String reason, Throwable cause)
{
if (LOG.isDebugEnabled())
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={}, reason={}, cause={})", coreRequest, coreResponse, callback, statusCode, reason, cause, cause);
HttpServletRequest request = getServletRequest(coreRequest);
HttpServletResponse response = getServletResponse(coreResponse);
try
{
if (isIncluded(request))
return;
if (cause != null)
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause);
response.sendError(statusCode, reason);
}
catch (IOException e)
{
// TODO: Need a better exception?
throw new RuntimeException(e);
}
finally
{
callback.succeeded();
}
}
@Override
protected boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException
{
boolean included = isIncluded(getServletRequest(request));
if (included)
return false;
return super.passConditionalHeaders(request, response, content, callback);
}
private HttpServletRequest getServletRequest(Request request)
{
ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class);
if (servletCoreRequest != null)
return servletCoreRequest.getServletRequest();
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
if (servletContextRequest != null)
return servletContextRequest.getServletApiRequest();
throw new IllegalStateException("instanceof " + request.getClass());
}
private HttpServletResponse getServletResponse(Response response)
{
ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class);
if (servletCoreResponse != null)
return servletCoreResponse.getServletResponse();
ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class);
if (servletContextResponse != null)
return servletContextResponse.getServletApiResponse();
throw new IllegalStateException("instanceof " + response.getClass());
}
}
static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly)
{
String servletPath = isPathInfoOnly ? "/" : includedServletPath;
String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
return URIUtil.addPaths(servletPath, pathInfo);
}
private static boolean isIncluded(HttpServletRequest request)
{
return request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
}
protected boolean isDefaultMapping(HttpServletRequest req)
{
if (req.getHttpServletMapping().getMappingMatch() == MappingMatch.DEFAULT)
return true;
return (req.getDispatcherType() != DispatcherType.REQUEST) && "default".equals(getServletConfig().getServletName());
}
/**
* Wrap an existing HttpContent with one that takes has an unknown/unspecified length.
*/
private static class UnknownLengthHttpContent extends HttpContent.Wrapper
{
public UnknownLengthHttpContent(HttpContent content)
{
super(content);
}
@Override
public HttpField getContentLength()
{
return null;
}
@Override
public long getContentLengthValue()
{
return -1;
}
}
private static class ForcedCharacterEncodingHttpContent extends HttpContent.Wrapper
{
private final String characterEncoding;
private final String contentType;
public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding)
{
super(Objects.requireNonNull(content));
this.characterEncoding = characterEncoding;
if (content.getContentTypeValue() == null || content.getResource().isDirectory())
{
this.contentType = null;
}
else
{
String mimeType = content.getContentTypeValue();
int idx = mimeType.indexOf(";charset");
if (idx >= 0)
mimeType = mimeType.substring(0, idx);
this.contentType = mimeType + ";charset=" + characterEncoding;
}
}
@Override
public HttpField getContentType()
{
return new HttpField(HttpHeader.CONTENT_TYPE, this.contentType);
}
@Override
public String getContentTypeValue()
{
return this.contentType;
}
@Override
public String getCharacterEncoding()
{
return this.characterEncoding;
}
}
/**
* <p>The different modes a welcome resource may be served by a Servlet.</p>
*/
private enum WelcomeServletMode
{
/**
* <p>Welcome targets are not served by Servlets.</p>
* <p>The welcome target must exist as a file on the filesystem.</p>
*/
NONE,
/**
* <p>Welcome target that exist as files on the filesystem are
* served, otherwise a matching Servlet may serve the welcome target.</p>
*/
MATCH,
/**
* <p>Welcome target that exist as files on the filesystem are
* served, otherwise an exact matching Servlet may serve the welcome target.</p>
*/
EXACT
}
private static class AsyncContextCallback implements Callback
{
private final AsyncContext _asyncContext;
private final HttpServletResponse _response;
private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response)
{
_asyncContext = asyncContext;
_response = response;
}
@Override
public void succeeded()
{
_asyncContext.complete();
}
@Override
public void failed(Throwable x)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("AsyncContextCallback failed {}", _asyncContext, x);
// It is known that this callback is only failed if the response is already committed,
// thus we can only abort the response here.
_response.sendError(-1);
}
catch (IOException e)
{
ExceptionUtil.addSuppressedIfNotAssociated(x, e);
}
finally
{
_asyncContext.complete();
}
if (LOG.isDebugEnabled())
LOG.debug("Async get failed", x);
}
return null;
}
}

View File

@ -258,6 +258,12 @@ public class Dispatcher implements RequestDispatcher
return null;
return vals.toArray(new String[0]);
}
@Override
public String toString()
{
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getRequest());
}
}
private class ForwardRequest extends ParameterRequestWrapper

View File

@ -0,0 +1,931 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.StringTokenizer;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.content.FileMappingHttpContentFactory;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory;
import org.eclipse.jetty.http.content.VirtualHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.ResourceService;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>A Servlet that handles static resources.</p>
* <p>The following init parameters are supported:</p>
* <dl>
* <dt>acceptRanges</dt>
* <dd>
* Use {@code true} to accept range requests, defaults to {@code true}.
* </dd>
* <dt>baseResource</dt>
* <dd>
* The root directory to look for static resources. Defaults to the context's baseResource. Relative URI
* are {@link Resource#resolve(String) resolved} against the context's {@link ServletContextHandler#getBaseResource()}
* base resource, all other values are resolved using {@link ServletContextHandler#newResource(String)}.
* </dd>
* <dt>cacheControl</dt>
* <dd>
* The value of the {@code Cache-Control} header.
* If omitted, no {@code Cache-Control} header is generated in responses.
* By default is omitted.
* </dd>
* <dt>cacheValidationTime</dt>
* <dd>
* How long in milliseconds a resource is cached.
* If omitted, defaults to {@code 1000} ms.
* Use {@code -1} to cache forever or {@code 0} to not cache.
* </dd>
* <dt>dirAllowed</dt>
* <dd>
* Use {@code true} to serve directory listing if no welcome file is found.
* Otherwise responds with {@code 403 Forbidden}.
* Defaults to {@code true}.
* </dd>
* <dt>encodingHeaderCacheSize</dt>
* <dd>
* Max number of cached {@code Accept-Encoding} entries.
* Use {@code -1} for the default value (100), {@code 0} for no cache.
* </dd>
* <dt>etags</dt>
* <dd>
* Use {@code true} to generate ETags in responses.
* Defaults to {@code false}.
* </dd>
* <dt>maxCachedFiles</dt>
* <dd>
* The max number of cached static resources.
* Use {@code -1} for the default value (2048) or {@code 0} for no cache.
* </dd>
* <dt>maxCachedFileSize</dt>
* <dd>
* The max size in bytes of a single cached static resource.
* Use {@code -1} for the default value (128 MiB) or {@code 0} for no cache.
* </dd>
* <dt>maxCacheSize</dt>
* <dd>
* The max size in bytes of the cache for static resources.
* Use {@code -1} for the default value (256 MiB) or {@code 0} for no cache.
* </dd>
* <dt>otherGzipFileExtensions</dt>
* <dd>
* A comma-separated list of extensions of files whose content is implicitly
* gzipped.
* Defaults to {@code .svgz}.
* </dd>
* <dt>pathInfoOnly</dt>
* <dd>
* Use {@code true} to use only the pathInfo portion of a PATH (aka prefix) match
* as obtained from {@link HttpServletRequest#getPathInfo()}.
* Defaults to {@code true}.
* </dd>
* <dt>precompressed</dt>
* <dd>
* Omitted by default, so that no pre-compressed content will be served.
* If set to {@code true}, the default set of pre-compressed formats will be used.
* Otherwise can be set to a comma-separated list of {@code encoding=extension} pairs,
* such as: {@code br=.br,gzip=.gz,bzip2=.bz}, where {@code encoding} is used as the
* value for the {@code Content-Encoding} header.
* </dd>
* <dt>redirectWelcome</dt>
* <dd>
* Use {@code true} to redirect welcome files, otherwise they are forwarded.
* Defaults to {@code false}.
* </dd>
* <dt>stylesheet</dt>
* <dd>
* Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
* The path of a custom stylesheet to style the directory listing HTML.
* </dd>
* <dt>useFileMappedBuffer</dt>
* <dd>
* Use {@code true} to use file mapping to serve static resources.
* Defaults to {@code false}.
* </dd>
* <dt>welcomeServlets</dt>
* <dd>
* Use {@code false} to only serve welcome resources from the file system.
* Use {@code true} to dispatch welcome resources to a matching Servlet
* (for example mapped to {@code *.welcome}), when the welcome resources
* does not exist on file system.
* Use {@code exact} to dispatch welcome resource to a Servlet whose mapping
* is exactly the same as the welcome resource (for example {@code /index.welcome}),
* when the welcome resources does not exist on file system.
* Defaults to {@code false}.
* </dd>
* </dl>
*/
public class ResourceServlet extends HttpServlet
{
private static final Logger LOG = LoggerFactory.getLogger(ResourceServlet.class);
private ServletResourceService _resourceService;
private WelcomeServletMode _welcomeServletMode;
private boolean _pathInfoOnly;
public ResourceService getResourceService()
{
return _resourceService;
}
@Override
public void init() throws ServletException
{
ServletContextHandler contextHandler = initContextHandler(getServletContext());
_resourceService = new ServletResourceService(contextHandler);
_resourceService.setWelcomeFactory(_resourceService);
Resource baseResource = contextHandler.getBaseResource();
String rb = getInitParameter("baseResource", "resourceBase");
if (rb != null)
{
try
{
baseResource = URIUtil.isRelative(rb) ? baseResource.resolve(rb) : contextHandler.newResource(rb);
if (baseResource.isAlias())
baseResource = contextHandler.newResource(baseResource.getRealURI());
}
catch (Exception e)
{
LOG.warn("Unable to create baseResource from {}", rb, e);
throw new UnavailableException(e.toString());
}
}
if (baseResource != null && !(baseResource.isDirectory() && baseResource.isReadable()))
LOG.warn("baseResource {} is not a readable directory", baseResource);
List<CompressedContentFormat> precompressedFormats = parsePrecompressedFormats(getInitParameter("precompressed"),
getInitBoolean("gzip"), _resourceService.getPrecompressedFormats());
// Try to get factory from ServletContext attribute.
HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName());
if (contentFactory == null)
{
MimeTypes mimeTypes = contextHandler.getMimeTypes();
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes);
// Use the servers default stylesheet unless there is one explicitly set by an init param.
Resource styleSheet = contextHandler.getServer().getDefaultStyleSheet();
String stylesheetParam = getInitParameter("stylesheet");
if (stylesheetParam != null)
{
try
{
HttpContent styleSheetContent = contentFactory.getContent(stylesheetParam);
Resource s = styleSheetContent == null ? null : styleSheetContent.getResource();
if (Resources.isReadableFile(s))
styleSheet = s;
else
LOG.warn("Stylesheet {} does not exist", stylesheetParam);
}
catch (Exception e)
{
if (LOG.isDebugEnabled())
LOG.warn("Unable to use stylesheet: {}", stylesheetParam, e);
else
LOG.warn("Unable to use stylesheet: {} - {}", stylesheetParam, e.toString());
}
}
if (getInitBoolean("useFileMappedBuffer", false))
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css");
contentFactory = new PreCompressedHttpContentFactory(contentFactory, precompressedFormats);
int maxCacheSize = getInitInt("maxCacheSize", -2);
int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
int maxCachedFiles = getInitInt("maxCachedFiles", -2);
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{
ByteBufferPool bufferPool = getByteBufferPool(contextHandler);
ValidatingCachingHttpContentFactory cached = new ValidatingCachingHttpContentFactory(contentFactory,
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
contentFactory = cached;
if (maxCacheSize >= 0)
cached.setMaxCacheSize(maxCacheSize);
if (maxCachedFileSize >= 0)
cached.setMaxCachedFileSize(maxCachedFileSize);
if (maxCachedFiles >= 0)
cached.setMaxCachedFiles(maxCachedFiles);
}
}
_resourceService.setHttpContentFactory(contentFactory);
if (contextHandler.getWelcomeFiles() == null)
contextHandler.setWelcomeFiles(new String[]{"index.html", "index.jsp"});
_resourceService.setAcceptRanges(getInitBoolean("acceptRanges", _resourceService.isAcceptRanges()));
_resourceService.setDirAllowed(getInitBoolean("dirAllowed", _resourceService.isDirAllowed()));
boolean redirectWelcome = getInitBoolean("redirectWelcome", false);
_resourceService.setWelcomeMode(redirectWelcome ? ResourceService.WelcomeMode.REDIRECT : ResourceService.WelcomeMode.SERVE);
_resourceService.setPrecompressedFormats(precompressedFormats);
_resourceService.setEtags(getInitBoolean("etags", _resourceService.isEtags()));
_welcomeServletMode = WelcomeServletMode.NONE;
String welcomeServlets = getInitParameter("welcomeServlets");
if (welcomeServlets != null)
{
welcomeServlets = welcomeServlets.toLowerCase(Locale.ENGLISH);
_welcomeServletMode = switch (welcomeServlets)
{
case "true" -> WelcomeServletMode.MATCH;
case "exact" -> WelcomeServletMode.EXACT;
default -> WelcomeServletMode.NONE;
};
}
int encodingHeaderCacheSize = getInitInt("encodingHeaderCacheSize", -1);
if (encodingHeaderCacheSize >= 0)
_resourceService.setEncodingCacheSize(encodingHeaderCacheSize);
String cc = getInitParameter("cacheControl");
if (cc != null)
_resourceService.setCacheControl(cc);
List<String> gzipEquivalentFileExtensions = new ArrayList<>();
String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
if (otherGzipExtensions != null)
{
//comma separated list
StringTokenizer tok = new StringTokenizer(otherGzipExtensions, ",", false);
while (tok.hasMoreTokens())
{
String s = tok.nextToken().trim();
gzipEquivalentFileExtensions.add((s.charAt(0) == '.' ? s : "." + s));
}
}
else
{
// .svgz files are gzipped svg files and must be served with Content-Encoding:gzip
gzipEquivalentFileExtensions.add(".svgz");
}
_resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions);
_pathInfoOnly = getInitBoolean("pathInfoOnly", true);
if (LOG.isDebugEnabled())
{
LOG.debug(" .baseResource = {}", baseResource);
LOG.debug(" .resourceService = {}", _resourceService);
LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode);
}
}
private static ByteBufferPool getByteBufferPool(ContextHandler contextHandler)
{
if (contextHandler == null)
return ByteBufferPool.NON_POOLING;
Server server = contextHandler.getServer();
if (server == null)
return ByteBufferPool.NON_POOLING;
return server.getByteBufferPool();
}
private String getInitParameter(String name, String... deprecated)
{
String value = getInitParameter(name);
if (value != null)
return value;
for (String d : deprecated)
{
value = getInitParameter(d);
if (value != null)
{
LOG.warn("Deprecated {} used instead of {}", d, name);
return value;
}
}
return null;
}
private List<CompressedContentFormat> parsePrecompressedFormats(String precompressed, Boolean gzip, List<CompressedContentFormat> dft)
{
if (precompressed == null && gzip == null)
{
return dft;
}
List<CompressedContentFormat> ret = new ArrayList<>();
if (precompressed != null && precompressed.indexOf('=') > 0)
{
for (String pair : precompressed.split(","))
{
String[] setting = pair.split("=");
String encoding = setting[0].trim();
String extension = setting[1].trim();
ret.add(new CompressedContentFormat(encoding, extension));
if (gzip == Boolean.TRUE && !ret.contains(CompressedContentFormat.GZIP))
ret.add(CompressedContentFormat.GZIP);
}
}
else if (precompressed != null)
{
if (Boolean.parseBoolean(precompressed))
{
ret.add(CompressedContentFormat.BR);
ret.add(CompressedContentFormat.GZIP);
}
}
else if (gzip == Boolean.TRUE)
{
// gzip handling is for backwards compatibility with older Jetty
ret.add(CompressedContentFormat.GZIP);
}
return ret;
}
private Boolean getInitBoolean(String name)
{
String value = getInitParameter(name);
if (value == null || value.isEmpty())
return null;
return (value.startsWith("t") ||
value.startsWith("T") ||
value.startsWith("y") ||
value.startsWith("Y") ||
value.startsWith("1"));
}
private boolean getInitBoolean(String name, boolean dft)
{
return Optional.ofNullable(getInitBoolean(name)).orElse(dft);
}
private int getInitInt(String name, int dft)
{
String value = getInitParameter(name);
if (value != null && !value.isEmpty())
return Integer.parseInt(value);
return dft;
}
protected ServletContextHandler initContextHandler(ServletContext servletContext)
{
if (servletContext instanceof ServletContextHandler.ServletContextApi api)
return api.getContext().getServletContextHandler();
Context context = ContextHandler.getCurrentContext();
if (context instanceof ContextHandler.ScopedContext scopedContext)
return scopedContext.getContextHandler();
throw new IllegalArgumentException("The servletContext " + servletContext + " " +
servletContext.getClass().getName() + " is not " + ContextHandler.ScopedContext.class.getName());
}
@Override
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
{
boolean included = httpServletRequest.getDispatcherType() == DispatcherType.INCLUDE;
String encodedPathInContext = getEncodedPathInContext(httpServletRequest, included);
if (LOG.isDebugEnabled())
LOG.debug("doGet(hsReq={}, hsResp={}) pathInContext={}, included={}", httpServletRequest, httpServletResponse, encodedPathInContext, included);
try
{
HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(httpServletRequest));
if (LOG.isDebugEnabled())
LOG.debug("content = {}", content);
if (content == null || Resources.missing(content.getResource()))
{
doNotFound(httpServletRequest, httpServletResponse, encodedPathInContext);
}
else
{
// lookup the core request and response as wrapped by the ServletContextHandler
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest);
ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse();
ServletChannel servletChannel = servletContextRequest.getServletChannel();
// If the servlet request has not been wrapped,
// we can use the core request directly,
// otherwise wrap the servlet request as a core request
Request coreRequest = httpServletRequest instanceof ServletApiRequest
? servletChannel.getRequest()
: ServletCoreRequest.wrap(httpServletRequest);
// If the servlet response has been wrapped and has been written to,
// then the servlet response must be wrapped as a core response
// otherwise we can use the core response directly.
boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming();
Response coreResponse = useServletResponse
? new ServletCoreResponse(coreRequest, httpServletResponse, included)
: servletChannel.getResponse();
// If the core response is already committed then do nothing more
if (coreResponse.isCommitted())
{
if (LOG.isDebugEnabled())
LOG.debug("Response already committed for {}", coreRequest.getHttpURI());
return;
}
// Get the content length before we may wrap the content
long contentLength = content.getContentLengthValue();
// Servlet Filters could be interacting with the Response already.
if (useServletResponse)
content = new UnknownLengthHttpContent(content);
// The character encoding may be forced
String characterEncoding = servletContextResponse.getRawCharacterEncoding();
if (characterEncoding != null)
content = new ForcedCharacterEncodingHttpContent(content, characterEncoding);
// If async is supported and the unwrapped content is larger than an output buffer
if (httpServletRequest.isAsyncSupported() &&
(contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()))
{
// send the content asynchronously
AsyncContext asyncContext = httpServletRequest.startAsync();
Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse);
_resourceService.doGet(coreRequest, coreResponse, callback, content);
}
else
{
// send the content blocking
try (Blocker.Callback callback = Blocker.callback())
{
_resourceService.doGet(coreRequest, coreResponse, callback, content);
callback.block();
}
catch (Exception e)
{
throw new ServletException(e);
}
}
}
}
catch (InvalidPathException e)
{
if (LOG.isDebugEnabled())
LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
if (included)
throw new FileNotFoundException(encodedPathInContext);
httpServletResponse.setStatus(404);
}
}
protected String getEncodedPathInContext(HttpServletRequest request, boolean included)
{
HttpServletMapping mapping = request.getHttpServletMapping();
if (included)
{
if (request.getAttribute(Dispatcher.INCLUDE_MAPPING) instanceof HttpServletMapping httpServletMapping)
{
mapping = httpServletMapping;
}
else
{
// must be an include of a named dispatcher. Just use the whole URI
return URIUtil.encodePath(URIUtil.addPaths(request.getServletPath(), request.getPathInfo()));
}
}
return switch (mapping.getMappingMatch())
{
case CONTEXT_ROOT -> "/";
case DEFAULT, EXTENSION, EXACT ->
{
if (included)
yield URIUtil.encodePath((String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH));
else if (request instanceof ServletApiRequest apiRequest)
// Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
yield Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
else
yield URIUtil.encodePath(request.getServletPath());
}
case PATH ->
{
if (_pathInfoOnly)
{
if (included)
yield URIUtil.encodePath((String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO));
else
yield URIUtil.encodePath(request.getPathInfo());
}
else
{
if (included)
yield URIUtil.encodePath(URIUtil.addPaths((String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH), (String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO)));
else if (request instanceof ServletApiRequest apiRequest)
// Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
yield Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
else
yield URIUtil.encodePath(URIUtil.addPaths(request.getServletPath(), request.getPathInfo()));
}
}
};
}
@Override
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
if (LOG.isDebugEnabled())
LOG.debug("doHead(req={}, resp={}) (calling doGet())", req, resp);
doGet(req, resp);
}
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
// Always return 405: Method Not Allowed for DefaultServlet
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
// override to eliminate TRACE that the default HttpServlet impl adds
resp.setHeader("Allow", "GET, HEAD, OPTIONS");
}
protected void doNotFound(HttpServletRequest request, HttpServletResponse response, String encodedPathInContext) throws IOException
{
if (request.getDispatcherType() == DispatcherType.INCLUDE)
{
/* https://github.com/jakartaee/servlet/blob/6.0.0-RELEASE/spec/src/main/asciidoc/servlet-spec-body.adoc#93-the-include-method
* 9.3 - If the default servlet is the target of a RequestDispatch.include() and the requested
* resource does not exist, then the default servlet MUST throw FileNotFoundException.
* If the exception isnt caught and handled, and the response
* hasnt been committed, the status code MUST be set to 500.
*/
throw new FileNotFoundException(encodedPathInContext);
}
// no content
response.sendError(404);
}
private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
{
private final ServletContextHandler _servletContextHandler;
private ServletResourceService(ServletContextHandler servletContextHandler)
{
_servletContextHandler = servletContextHandler;
}
@Override
public String getWelcomeTarget(HttpContent content, Request coreRequest)
{
String[] welcomes = _servletContextHandler.getWelcomeFiles();
if (welcomes == null)
return null;
String pathInContext = Request.getPathInContext(coreRequest);
String welcomeTarget = null;
Resource base = content.getResource();
if (Resources.isReadableDirectory(base))
{
for (String welcome : welcomes)
{
String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
// If the welcome resource is a file, it has
// precedence over resources served by Servlets.
Resource welcomePath = content.getResource().resolve(welcome);
if (Resources.isReadableFile(welcomePath))
return welcomeInContext;
// Check whether a Servlet may serve the welcome resource.
if (_welcomeServletMode != WelcomeServletMode.NONE && welcomeTarget == null)
{
ServletHandler.MappedServlet entry = _servletContextHandler.getServletHandler().getMappedServlet(welcomeInContext);
// Is there a different Servlet that may serve the welcome resource?
if (entry != null && entry.getServletHolder().getServletInstance() != ResourceServlet.this)
{
if (_welcomeServletMode == WelcomeServletMode.MATCH || entry.getPathSpec().getDeclaration().equals(welcomeInContext))
{
welcomeTarget = welcomeInContext;
// Do not break the loop, because we want to try other welcome resources
// that may be files and take precedence over Servlet welcome resources.
}
}
}
}
}
return welcomeTarget;
}
@Override
protected void serveWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
{
HttpServletRequest servletRequest = getServletRequest(request);
HttpServletResponse servletResponse = getServletResponse(response);
boolean included = isIncluded(servletRequest);
RequestDispatcher dispatcher = servletRequest.getServletContext().getRequestDispatcher(welcomeTarget);
if (dispatcher == null)
{
// We know that the welcome target exists and can be served.
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}
try
{
if (included)
{
dispatcher.include(servletRequest, servletResponse);
}
else
{
servletRequest.setAttribute("org.eclipse.jetty.server.welcome", welcomeTarget);
dispatcher.forward(servletRequest, servletResponse);
}
callback.succeeded();
}
catch (ServletException e)
{
callback.failed(e);
}
}
@Override
protected void rehandleWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
{
serveWelcome(request, response, callback, welcomeTarget);
}
@Override
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode)
{
if (LOG.isDebugEnabled())
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={})", coreRequest, coreResponse, callback, statusCode);
writeHttpError(coreRequest, coreResponse, callback, statusCode, null, null);
}
@Override
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, Throwable cause)
{
if (LOG.isDebugEnabled())
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, cause={})", coreRequest, coreResponse, callback, cause, cause);
int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500;
String reason = null;
if (cause instanceof HttpException httpException)
{
statusCode = httpException.getCode();
reason = httpException.getReason();
}
writeHttpError(coreRequest, coreResponse, callback, statusCode, reason, cause);
}
@Override
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode, String reason, Throwable cause)
{
if (LOG.isDebugEnabled())
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={}, reason={}, cause={})", coreRequest, coreResponse, callback, statusCode, reason, cause, cause);
HttpServletRequest request = getServletRequest(coreRequest);
HttpServletResponse response = getServletResponse(coreResponse);
try
{
if (isIncluded(request))
return;
if (cause != null)
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause);
response.sendError(statusCode, reason);
}
catch (IOException e)
{
// TODO: Need a better exception?
throw new RuntimeException(e);
}
finally
{
callback.succeeded();
}
}
@Override
protected boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException
{
boolean included = isIncluded(getServletRequest(request));
if (included)
return false;
return super.passConditionalHeaders(request, response, content, callback);
}
private HttpServletRequest getServletRequest(Request request)
{
ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class);
if (servletCoreRequest != null)
return servletCoreRequest.getServletRequest();
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
if (servletContextRequest != null)
return servletContextRequest.getServletApiRequest();
throw new IllegalStateException("instanceof " + request.getClass());
}
private HttpServletResponse getServletResponse(Response response)
{
ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class);
if (servletCoreResponse != null)
return servletCoreResponse.getServletResponse();
ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class);
if (servletContextResponse != null)
return servletContextResponse.getServletApiResponse();
throw new IllegalStateException("instanceof " + response.getClass());
}
}
static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath)
{
String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
return URIUtil.addPaths(includedServletPath, pathInfo);
}
private static boolean isIncluded(HttpServletRequest request)
{
return request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
}
/**
* Wrap an existing HttpContent with one that takes has an unknown/unspecified length.
*/
private static class UnknownLengthHttpContent extends HttpContent.Wrapper
{
public UnknownLengthHttpContent(HttpContent content)
{
super(content);
}
@Override
public HttpField getContentLength()
{
return null;
}
@Override
public long getContentLengthValue()
{
return -1;
}
}
private static class ForcedCharacterEncodingHttpContent extends HttpContent.Wrapper
{
private final String characterEncoding;
private final String contentType;
public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding)
{
super(Objects.requireNonNull(content));
this.characterEncoding = characterEncoding;
if (content.getContentTypeValue() == null || content.getResource().isDirectory())
{
this.contentType = null;
}
else
{
String mimeType = content.getContentTypeValue();
int idx = mimeType.indexOf(";charset");
if (idx >= 0)
mimeType = mimeType.substring(0, idx);
this.contentType = mimeType + ";charset=" + characterEncoding;
}
}
@Override
public HttpField getContentType()
{
return new HttpField(HttpHeader.CONTENT_TYPE, this.contentType);
}
@Override
public String getContentTypeValue()
{
return this.contentType;
}
@Override
public String getCharacterEncoding()
{
return this.characterEncoding;
}
}
/**
* <p>The different modes a welcome resource may be served by a Servlet.</p>
*/
private enum WelcomeServletMode
{
/**
* <p>Welcome targets are not served by Servlets.</p>
* <p>The welcome target must exist as a file on the filesystem.</p>
*/
NONE,
/**
* <p>Welcome target that exist as files on the filesystem are
* served, otherwise a matching Servlet may serve the welcome target.</p>
*/
MATCH,
/**
* <p>Welcome target that exist as files on the filesystem are
* served, otherwise an exact matching Servlet may serve the welcome target.</p>
*/
EXACT
}
private static class AsyncContextCallback implements Callback
{
private final AsyncContext _asyncContext;
private final HttpServletResponse _response;
private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response)
{
_asyncContext = asyncContext;
_response = response;
}
@Override
public void succeeded()
{
_asyncContext.complete();
}
@Override
public void failed(Throwable x)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("AsyncContextCallback failed {}", _asyncContext, x);
// It is known that this callback is only failed if the response is already committed,
// thus we can only abort the response here.
_response.sendError(-1);
}
catch (IOException e)
{
ExceptionUtil.addSuppressedIfNotAssociated(x, e);
}
finally
{
_asyncContext.complete();
}
if (LOG.isDebugEnabled())
LOG.debug("Async get failed", x);
}
}
}

View File

@ -1433,6 +1433,12 @@ public class ServletApiRequest implements HttpServletRequest
return trailersMap;
}
@Override
public String toString()
{
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), _servletContextRequest);
}
static class AmbiguousURI extends ServletApiRequest
{
private final String msg;

View File

@ -100,7 +100,7 @@ public class ServletCoreRequest implements Request
.authority(request.getServerName(), request.getServerPort());
if (included)
builder.path(addEncodedPaths(request.getContextPath(), encodePath(DefaultServlet.getIncludedPathInContext(request, includedServletPath, false))));
builder.path(addEncodedPaths(request.getContextPath(), encodePath(ResourceServlet.getIncludedPathInContext(request, includedServletPath))));
else if (request.getDispatcherType() != DispatcherType.REQUEST)
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
else

View File

@ -38,6 +38,7 @@ import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
@ -52,7 +53,6 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.logging.StacklessLogging;
@ -1383,7 +1383,7 @@ public class DefaultServletTest
FS.ensureDirExists(altRoot);
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/alt/*");
defholder.setInitParameter("resourceBase", altRoot.toUri().toASCIIString());
defholder.setInitParameter("baseResource", altRoot.toUri().toASCIIString());
defholder.setInitParameter("dirAllowed", "false");
defholder.setInitParameter("redirectWelcome", "false");
defholder.setInitParameter("welcomeServlets", "true");
@ -1394,7 +1394,8 @@ public class DefaultServletTest
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String includeTarget = req.getParameter("includeTarget");
req.getRequestDispatcher(includeTarget).include(req, resp);
RequestDispatcher requestDispatcher = req.getRequestDispatcher(includeTarget);
requestDispatcher.include(req, resp);
}
});
context.addServlet(gwholder, "/gateway");
@ -2001,10 +2002,9 @@ public class DefaultServletTest
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContent(), containsString("<h1>Hello World</h1>"));
ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
/*
TODO: fix after HttpContent changes.
ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
HttpContent content = factory.getContent("/index.html", 200);
ByteBuffer buffer = content.getDirectBuffer();
assertThat("Buffer is direct", buffer.isDirect(), is(true));
@ -3425,22 +3425,17 @@ public class DefaultServletTest
response.setCharacterEncoding("utf-8");
chain.doFilter(request, response);
}
@Override
public void destroy()
{
}
}
@Test
public void testPathInfoOnly() throws Exception
public void testNotPathInfoOnly() throws Exception
{
ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS);
context.setWelcomeFiles(new String[]{"index.y", "index.x"});
ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet()
{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
@ -3456,27 +3451,27 @@ public class DefaultServletTest
context.getServletHandler().addServlet(indexServlet);
context.getServletHandler().addServletMapping(indexMapping);
Path pathTest = MavenTestingUtils.getTestResourcePath("pathTest");
Path docroot = MavenTestingUtils.getTestResourcePath("docroot");
Path defaultDir = pathTest.resolve("default");
ServletHolder slashHolder = new ServletHolder("default", new DefaultServlet());
slashHolder.setInitParameter("redirectWelcome", "false");
slashHolder.setInitParameter("welcomeServlets", "true");
slashHolder.setInitParameter("baseResource", defaultDir.toAbsolutePath().toString());
slashHolder.setInitParameter("baseResource", docroot.toAbsolutePath().toString());
context.addServlet(slashHolder, "/");
Path rDir = pathTest.resolve("rdir");
ServletHolder rHolder = new ServletHolder("rdefault", new DefaultServlet());
Path altroot = MavenTestingUtils.getTestResourcePath("altroot");
ServletHolder rHolder = new ServletHolder("alt", new DefaultServlet());
rHolder.setInitParameter("redirectWelcome", "false");
rHolder.setInitParameter("welcomeServlets", "true");
rHolder.setInitParameter("baseResource", rDir.toAbsolutePath().toString());
context.addServlet(rHolder, "/r/*");
rHolder.setInitParameter("pathInfoOnly", "false");
rHolder.setInitParameter("baseResource", altroot.toAbsolutePath().toString());
context.addServlet(rHolder, "/all/*");
server.stop();
server.setHandler(context);
server.start();
String rawRequest = """
GET /c1/r/ HTTP/1.1\r
GET /c1/all/index.html HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
@ -3484,7 +3479,7 @@ public class DefaultServletTest
String rawResponse = connector.getResponse(rawRequest);
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.getContent(), containsString("testPathInfoOnly-OK"));
assertThat(response.getContent(), containsString("this is alternate index content"));
}
@Test
@ -3652,11 +3647,6 @@ public class DefaultServletTest
response.setCharacterEncoding("utf-8");
chain.doFilter(request, response);
}
@Override
public void destroy()
{
}
}
private boolean deleteFile(Path file) throws IOException

View File

@ -0,0 +1 @@
this is alternate index content.

View File

@ -1 +0,0 @@
this is alt content.

View File

@ -0,0 +1 @@
THIS CANNOT BE SEEN

View File

@ -72,7 +72,6 @@
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Reuse Forks causes test failures -->
<reuseForks>true</reuseForks>
<argLine>@{argLine} ${jetty.surefire.argLine}
--add-modules org.eclipse.jetty.util.ajax

View File

@ -25,6 +25,7 @@ import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.awaitility.Awaitility;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.ee9.nested.ContextHandler;
@ -125,6 +126,7 @@ public class ContextScopeListenerTest
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/initialPath");
ContentResponse response = _client.GET(uri);
assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(() -> _history.size() == 7);
assertHistory(
"enterScope /initialPath",
"doGet",