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:
parent
29d27f7373
commit
46204a86ff
|
@ -5,7 +5,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.eclipse.jetty.documentation</groupId>
|
<groupId>org.eclipse.jetty.documentation</groupId>
|
||||||
<artifactId>documentation</artifactId>
|
<artifactId>documentation</artifactId>
|
||||||
<version>12.0.10-SNAPSHOT</version>
|
<version>12.0.12-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>jetty-asciidoctor-extensions</artifactId>
|
<artifactId>jetty-asciidoctor-extensions</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.eclipse.jetty.documentation</groupId>
|
<groupId>org.eclipse.jetty.documentation</groupId>
|
||||||
<artifactId>documentation</artifactId>
|
<artifactId>documentation</artifactId>
|
||||||
<version>12.0.10-SNAPSHOT</version>
|
<version>12.0.12-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>jetty-documentation</artifactId>
|
<artifactId>jetty-documentation</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>Documentation :: Guides</name>
|
<name>Documentation :: Guides</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
@ -49,15 +49,20 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-home</artifactId>
|
<artifactId>jetty-infinispan-common</artifactId>
|
||||||
<version>${project.version}</version>
|
|
||||||
<type>zip</type>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-infinispan-embedded-query</artifactId>
|
<artifactId>jetty-infinispan-embedded-query</artifactId>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-jmx</artifactId>
|
<artifactId>jetty-jmx</artifactId>
|
||||||
|
|
|
@ -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]
|
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]
|
||||||
|
----
|
|
@ -34,6 +34,7 @@ import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
||||||
import org.eclipse.jetty.client.ContentResponse;
|
import org.eclipse.jetty.client.ContentResponse;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
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.ServletContextHandler;
|
||||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||||
|
@ -1220,11 +1221,32 @@ public class HTTPServerDocs
|
||||||
// Add the DefaultServlet to serve static content.
|
// Add the DefaultServlet to serve static content.
|
||||||
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
|
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
|
||||||
// Configure the DefaultServlet with init-parameters.
|
// Configure the DefaultServlet with init-parameters.
|
||||||
servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/");
|
servletHolder.setInitParameter("maxCacheSize", "8388608");
|
||||||
|
servletHolder.setInitParameter("dirAllowed", "true");
|
||||||
servletHolder.setAsyncSupported(true);
|
servletHolder.setAsyncSupported(true);
|
||||||
// end::defaultServlet[]
|
// 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
|
public void serverGzipHandler() throws Exception
|
||||||
{
|
{
|
||||||
// tag::serverGzipHandler[]
|
// tag::serverGzipHandler[]
|
||||||
|
|
|
@ -1220,7 +1220,7 @@ public class HTTPServerDocs
|
||||||
// Add the DefaultServlet to serve static content.
|
// Add the DefaultServlet to serve static content.
|
||||||
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
|
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
|
||||||
// Configure the DefaultServlet with init-parameters.
|
// Configure the DefaultServlet with init-parameters.
|
||||||
servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/");
|
servletHolder.setInitParameter("baseResource", "/path/to/static/resources/");
|
||||||
servletHolder.setAsyncSupported(true);
|
servletHolder.setAsyncSupported(true);
|
||||||
// end::defaultServlet[]
|
// end::defaultServlet[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,7 @@
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>jetty</module>
|
<module>jetty</module>
|
||||||
|
<module>jetty-asciidoctor-extensions</module>
|
||||||
|
<module>jetty-documentation</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -919,6 +919,12 @@ public interface Request extends Attributes, Content.Source
|
||||||
{
|
{
|
||||||
return _request;
|
return _request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getWrapped());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|
|
@ -13,374 +13,36 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.ee10.servlet;
|
package org.eclipse.jetty.ee10.servlet;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
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.ServletContext;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.UnavailableException;
|
|
||||||
import jakarta.servlet.http.HttpServlet;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.servlet.http.MappingMatch;
|
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.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.URIUtil;
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.eclipse.jetty.util.resource.Resources;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 {@code DefaultServlet}, is a specialization of the {@link ResourceServlet} to be mapped to {@code /} as the "default"
|
||||||
* <p>The following init parameters are supported:</p>
|
* servlet for a context.
|
||||||
* <dl>
|
* </p>
|
||||||
* <dt>acceptRanges</dt>
|
* <p>
|
||||||
* <dd>
|
* In addition to the servlet init parameters that can be used to configure any {@link ResourceServlet}, the DefaultServlet
|
||||||
* Use {@code true} to accept range requests, defaults to {@code true}.
|
* also looks at {@link ServletContext#getInitParameter(String)} for any parameter starting with {@link #CONTEXT_INIT}, which
|
||||||
* </dd>
|
* is then stripped and the resulting name interpreted as a {@link ResourceServlet} init parameter.
|
||||||
* <dt>baseResource</dt>
|
* </p>
|
||||||
* <dd>
|
* <p>
|
||||||
* Defaults to the context's baseResource.
|
* To serve static content other than as the {@code DefaultServlet} mapped to "/", please use the {@link ResourceServlet} directly.
|
||||||
* The root directory to look for static resources.
|
* The {@code DefaultServlet} will warn if it is used other than as the default servlet. In future, this may become a fatal error.
|
||||||
* </dd>
|
* </p>
|
||||||
* <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>
|
|
||||||
*/
|
*/
|
||||||
public class DefaultServlet extends HttpServlet
|
public class DefaultServlet extends ResourceServlet
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultServlet.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DefaultServlet.class);
|
||||||
public static final String CONTEXT_INIT = "org.eclipse.jetty.servlet.Default.";
|
public static final String CONTEXT_INIT = "org.eclipse.jetty.servlet.Default.";
|
||||||
|
private final AtomicBoolean warned = new AtomicBoolean(false);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -405,519 +67,55 @@ public class DefaultServlet extends HttpServlet
|
||||||
return value;
|
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
|
@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);
|
if ("true".equalsIgnoreCase(getInitParameter("pathInfoOnly")))
|
||||||
String encodedPathInContext = getEncodedPathInContext(httpServletRequest, includedServletPath);
|
LOG.warn("DefaultServlet pathInfoOnly is set to true. Use ResourceServlet instead.");
|
||||||
boolean included = includedServletPath != null;
|
super.init();
|
||||||
|
|
||||||
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 isn’t caught and handled, and the response
|
|
||||||
* hasn’t 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath)
|
||||||
{
|
{
|
||||||
if (includedServletPath != null)
|
return 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,6 +258,12 @@ public class Dispatcher implements RequestDispatcher
|
||||||
return null;
|
return null;
|
||||||
return vals.toArray(new String[0]);
|
return vals.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getRequest());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ForwardRequest extends ParameterRequestWrapper
|
private class ForwardRequest extends ParameterRequestWrapper
|
||||||
|
|
|
@ -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 isn’t caught and handled, and the response
|
||||||
|
* hasn’t 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1433,6 +1433,12 @@ public class ServletApiRequest implements HttpServletRequest
|
||||||
return trailersMap;
|
return trailersMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), _servletContextRequest);
|
||||||
|
}
|
||||||
|
|
||||||
static class AmbiguousURI extends ServletApiRequest
|
static class AmbiguousURI extends ServletApiRequest
|
||||||
{
|
{
|
||||||
private final String msg;
|
private final String msg;
|
||||||
|
|
|
@ -100,7 +100,7 @@ public class ServletCoreRequest implements Request
|
||||||
.authority(request.getServerName(), request.getServerPort());
|
.authority(request.getServerName(), request.getServerPort());
|
||||||
|
|
||||||
if (included)
|
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)
|
else if (request.getDispatcherType() != DispatcherType.REQUEST)
|
||||||
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
|
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
|
||||||
else
|
else
|
||||||
|
|
|
@ -38,6 +38,7 @@ import jakarta.servlet.DispatcherType;
|
||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.FilterConfig;
|
import jakarta.servlet.FilterConfig;
|
||||||
|
import jakarta.servlet.RequestDispatcher;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import jakarta.servlet.ServletResponse;
|
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.HttpTester;
|
||||||
import org.eclipse.jetty.http.UriCompliance;
|
import org.eclipse.jetty.http.UriCompliance;
|
||||||
import org.eclipse.jetty.http.content.ResourceHttpContent;
|
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.ByteBufferPool;
|
||||||
import org.eclipse.jetty.io.IOResources;
|
import org.eclipse.jetty.io.IOResources;
|
||||||
import org.eclipse.jetty.logging.StacklessLogging;
|
import org.eclipse.jetty.logging.StacklessLogging;
|
||||||
|
@ -1383,7 +1383,7 @@ public class DefaultServletTest
|
||||||
FS.ensureDirExists(altRoot);
|
FS.ensureDirExists(altRoot);
|
||||||
|
|
||||||
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/alt/*");
|
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("dirAllowed", "false");
|
||||||
defholder.setInitParameter("redirectWelcome", "false");
|
defholder.setInitParameter("redirectWelcome", "false");
|
||||||
defholder.setInitParameter("welcomeServlets", "true");
|
defholder.setInitParameter("welcomeServlets", "true");
|
||||||
|
@ -1394,7 +1394,8 @@ public class DefaultServletTest
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||||
{
|
{
|
||||||
String includeTarget = req.getParameter("includeTarget");
|
String includeTarget = req.getParameter("includeTarget");
|
||||||
req.getRequestDispatcher(includeTarget).include(req, resp);
|
RequestDispatcher requestDispatcher = req.getRequestDispatcher(includeTarget);
|
||||||
|
requestDispatcher.include(req, resp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
context.addServlet(gwholder, "/gateway");
|
context.addServlet(gwholder, "/gateway");
|
||||||
|
@ -2001,10 +2002,9 @@ public class DefaultServletTest
|
||||||
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
|
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
|
||||||
assertThat(response.getContent(), containsString("<h1>Hello World</h1>"));
|
assertThat(response.getContent(), containsString("<h1>Hello World</h1>"));
|
||||||
|
|
||||||
ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: fix after HttpContent changes.
|
TODO: fix after HttpContent changes.
|
||||||
|
ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
|
||||||
HttpContent content = factory.getContent("/index.html", 200);
|
HttpContent content = factory.getContent("/index.html", 200);
|
||||||
ByteBuffer buffer = content.getDirectBuffer();
|
ByteBuffer buffer = content.getDirectBuffer();
|
||||||
assertThat("Buffer is direct", buffer.isDirect(), is(true));
|
assertThat("Buffer is direct", buffer.isDirect(), is(true));
|
||||||
|
@ -3425,22 +3425,17 @@ public class DefaultServletTest
|
||||||
response.setCharacterEncoding("utf-8");
|
response.setCharacterEncoding("utf-8");
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPathInfoOnly() throws Exception
|
public void testNotPathInfoOnly() throws Exception
|
||||||
{
|
{
|
||||||
ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS);
|
ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS);
|
||||||
context.setWelcomeFiles(new String[]{"index.y", "index.x"});
|
context.setWelcomeFiles(new String[]{"index.y", "index.x"});
|
||||||
ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet()
|
ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet()
|
||||||
{
|
{
|
||||||
@Override
|
@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.setContentType("text/plain");
|
||||||
resp.setCharacterEncoding("UTF-8");
|
resp.setCharacterEncoding("UTF-8");
|
||||||
|
@ -3456,27 +3451,27 @@ public class DefaultServletTest
|
||||||
context.getServletHandler().addServlet(indexServlet);
|
context.getServletHandler().addServlet(indexServlet);
|
||||||
context.getServletHandler().addServletMapping(indexMapping);
|
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());
|
ServletHolder slashHolder = new ServletHolder("default", new DefaultServlet());
|
||||||
slashHolder.setInitParameter("redirectWelcome", "false");
|
slashHolder.setInitParameter("redirectWelcome", "false");
|
||||||
slashHolder.setInitParameter("welcomeServlets", "true");
|
slashHolder.setInitParameter("welcomeServlets", "true");
|
||||||
slashHolder.setInitParameter("baseResource", defaultDir.toAbsolutePath().toString());
|
slashHolder.setInitParameter("baseResource", docroot.toAbsolutePath().toString());
|
||||||
context.addServlet(slashHolder, "/");
|
context.addServlet(slashHolder, "/");
|
||||||
|
|
||||||
Path rDir = pathTest.resolve("rdir");
|
Path altroot = MavenTestingUtils.getTestResourcePath("altroot");
|
||||||
ServletHolder rHolder = new ServletHolder("rdefault", new DefaultServlet());
|
ServletHolder rHolder = new ServletHolder("alt", new DefaultServlet());
|
||||||
rHolder.setInitParameter("redirectWelcome", "false");
|
rHolder.setInitParameter("redirectWelcome", "false");
|
||||||
rHolder.setInitParameter("welcomeServlets", "true");
|
rHolder.setInitParameter("welcomeServlets", "true");
|
||||||
rHolder.setInitParameter("baseResource", rDir.toAbsolutePath().toString());
|
rHolder.setInitParameter("pathInfoOnly", "false");
|
||||||
context.addServlet(rHolder, "/r/*");
|
rHolder.setInitParameter("baseResource", altroot.toAbsolutePath().toString());
|
||||||
|
context.addServlet(rHolder, "/all/*");
|
||||||
|
|
||||||
server.stop();
|
server.stop();
|
||||||
server.setHandler(context);
|
server.setHandler(context);
|
||||||
server.start();
|
server.start();
|
||||||
String rawRequest = """
|
String rawRequest = """
|
||||||
GET /c1/r/ HTTP/1.1\r
|
GET /c1/all/index.html HTTP/1.1\r
|
||||||
Host: localhost\r
|
Host: localhost\r
|
||||||
Connection: close\r
|
Connection: close\r
|
||||||
\r
|
\r
|
||||||
|
@ -3484,7 +3479,7 @@ public class DefaultServletTest
|
||||||
|
|
||||||
String rawResponse = connector.getResponse(rawRequest);
|
String rawResponse = connector.getResponse(rawRequest);
|
||||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||||
assertThat(response.getContent(), containsString("testPathInfoOnly-OK"));
|
assertThat(response.getContent(), containsString("this is alternate index content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -3652,11 +3647,6 @@ public class DefaultServletTest
|
||||||
response.setCharacterEncoding("utf-8");
|
response.setCharacterEncoding("utf-8");
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean deleteFile(Path file) throws IOException
|
private boolean deleteFile(Path file) throws IOException
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
this is alternate index content.
|
|
@ -1 +0,0 @@
|
||||||
this is alt content.
|
|
|
@ -0,0 +1 @@
|
||||||
|
THIS CANNOT BE SEEN
|
|
@ -72,7 +72,6 @@
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!-- Reuse Forks causes test failures -->
|
|
||||||
<reuseForks>true</reuseForks>
|
<reuseForks>true</reuseForks>
|
||||||
<argLine>@{argLine} ${jetty.surefire.argLine}
|
<argLine>@{argLine} ${jetty.surefire.argLine}
|
||||||
--add-modules org.eclipse.jetty.util.ajax
|
--add-modules org.eclipse.jetty.util.ajax
|
||||||
|
|
|
@ -25,6 +25,7 @@ import jakarta.servlet.DispatcherType;
|
||||||
import jakarta.servlet.http.HttpServlet;
|
import jakarta.servlet.http.HttpServlet;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.awaitility.Awaitility;
|
||||||
import org.eclipse.jetty.client.ContentResponse;
|
import org.eclipse.jetty.client.ContentResponse;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.ee9.nested.ContextHandler;
|
import org.eclipse.jetty.ee9.nested.ContextHandler;
|
||||||
|
@ -125,6 +126,7 @@ public class ContextScopeListenerTest
|
||||||
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/initialPath");
|
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/initialPath");
|
||||||
ContentResponse response = _client.GET(uri);
|
ContentResponse response = _client.GET(uri);
|
||||||
assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
|
assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
|
||||||
|
Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(() -> _history.size() == 7);
|
||||||
assertHistory(
|
assertHistory(
|
||||||
"enterScope /initialPath",
|
"enterScope /initialPath",
|
||||||
"doGet",
|
"doGet",
|
||||||
|
|
Loading…
Reference in New Issue