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>
|
||||
<groupId>org.eclipse.jetty.documentation</groupId>
|
||||
<artifactId>documentation</artifactId>
|
||||
<version>12.0.10-SNAPSHOT</version>
|
||||
<version>12.0.12-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>jetty-asciidoctor-extensions</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
<parent>
|
||||
<groupId>org.eclipse.jetty.documentation</groupId>
|
||||
<artifactId>documentation</artifactId>
|
||||
<version>12.0.10-SNAPSHOT</version>
|
||||
<version>12.0.12-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>jetty-documentation</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<packaging>jar</packaging>
|
||||
<name>Documentation :: Guides</name>
|
||||
|
||||
<properties>
|
||||
|
@ -49,15 +49,20 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-home</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>zip</type>
|
||||
<optional>true</optional>
|
||||
<artifactId>jetty-infinispan-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-infinispan-embedded-query</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-infinispan-remote-query</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-jmx</artifactId>
|
||||
|
|
|
@ -654,3 +654,10 @@ Below you can find an example of how to setup `DefaultServlet`:
|
|||
----
|
||||
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultServlet]
|
||||
----
|
||||
|
||||
If you wish to serve resources from a non-default location, then the `ResourceServlet` should be used instead of the `DefaultServlet`. Below you can find an example of how to setup both a `DefaultServlet` and a `ResourceServlet`:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=resourceServlet]
|
||||
----
|
|
@ -34,6 +34,7 @@ import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
|||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.ee10.servlet.ResourceServlet;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
|
@ -1220,11 +1221,32 @@ public class HTTPServerDocs
|
|||
// Add the DefaultServlet to serve static content.
|
||||
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
|
||||
// Configure the DefaultServlet with init-parameters.
|
||||
servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/");
|
||||
servletHolder.setInitParameter("maxCacheSize", "8388608");
|
||||
servletHolder.setInitParameter("dirAllowed", "true");
|
||||
servletHolder.setAsyncSupported(true);
|
||||
// end::defaultServlet[]
|
||||
}
|
||||
|
||||
public void resourceServlet()
|
||||
{
|
||||
// tag::resourceServlet[]
|
||||
// Create a ServletContextHandler with contextPath.
|
||||
ServletContextHandler context = new ServletContextHandler();
|
||||
context.setContextPath("/app");
|
||||
|
||||
// Add the ResourceServlet to serve static content from a specific location.
|
||||
ServletHolder servletHolder = context.addServlet(ResourceServlet.class, "/static/*");
|
||||
// Configure the ResourceServlet with init-parameters.
|
||||
servletHolder.setInitParameter("baseResource", "/absolute/path/to/static/resources/");
|
||||
servletHolder.setInitParameter("pathInfoOnly", "true");
|
||||
servletHolder.setAsyncSupported(true);
|
||||
|
||||
// Add the DefaultServlet to serve static content from the resource base
|
||||
ServletHolder defaultHolder = context.addServlet(DefaultServlet.class, "/");
|
||||
defaultHolder.setAsyncSupported(true);
|
||||
// end::resourceServlet[]
|
||||
}
|
||||
|
||||
public void serverGzipHandler() throws Exception
|
||||
{
|
||||
// tag::serverGzipHandler[]
|
||||
|
|
|
@ -1220,7 +1220,7 @@ public class HTTPServerDocs
|
|||
// Add the DefaultServlet to serve static content.
|
||||
ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/");
|
||||
// Configure the DefaultServlet with init-parameters.
|
||||
servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/");
|
||||
servletHolder.setInitParameter("baseResource", "/path/to/static/resources/");
|
||||
servletHolder.setAsyncSupported(true);
|
||||
// end::defaultServlet[]
|
||||
}
|
||||
|
|
|
@ -14,5 +14,7 @@
|
|||
|
||||
<modules>
|
||||
<module>jetty</module>
|
||||
<module>jetty-asciidoctor-extensions</module>
|
||||
<module>jetty-documentation</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
|
@ -919,6 +919,12 @@ public interface Request extends Attributes, Content.Source
|
|||
{
|
||||
return _request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getWrapped());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
|
@ -13,374 +13,36 @@
|
|||
|
||||
package org.eclipse.jetty.ee10.servlet;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.UnavailableException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.MappingMatch;
|
||||
import org.eclipse.jetty.http.CompressedContentFormat;
|
||||
import org.eclipse.jetty.http.HttpException;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.content.FileMappingHttpContentFactory;
|
||||
import org.eclipse.jetty.http.content.HttpContent;
|
||||
import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory;
|
||||
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
|
||||
import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory;
|
||||
import org.eclipse.jetty.http.content.VirtualHttpContentFactory;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.server.Context;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.ResourceService;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.util.Blocker;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.ExceptionUtil;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.eclipse.jetty.util.resource.Resources;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.eclipse.jetty.util.URIUtil.encodePath;
|
||||
|
||||
/**
|
||||
* <p>The default Servlet, normally mapped to {@code /}, that handles static resources.</p>
|
||||
* <p>The following init parameters are supported:</p>
|
||||
* <dl>
|
||||
* <dt>acceptRanges</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to accept range requests, defaults to {@code true}.
|
||||
* </dd>
|
||||
* <dt>baseResource</dt>
|
||||
* <dd>
|
||||
* Defaults to the context's baseResource.
|
||||
* The root directory to look for static resources.
|
||||
* </dd>
|
||||
* <dt>cacheControl</dt>
|
||||
* <dd>
|
||||
* The value of the {@code Cache-Control} header.
|
||||
* If omitted, no {@code Cache-Control} header is generated in responses.
|
||||
* By default is omitted.
|
||||
* </dd>
|
||||
* <dt>cacheValidationTime</dt>
|
||||
* <dd>
|
||||
* How long in milliseconds a resource is cached.
|
||||
* If omitted, defaults to {@code 1000} ms.
|
||||
* Use {@code -1} to cache forever or {@code 0} to not cache.
|
||||
* </dd>
|
||||
* <dt>dirAllowed</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to serve directory listing if no welcome file is found.
|
||||
* Otherwise responds with {@code 403 Forbidden}.
|
||||
* Defaults to {@code true}.
|
||||
* </dd>
|
||||
* <dt>encodingHeaderCacheSize</dt>
|
||||
* <dd>
|
||||
* Max number of cached {@code Accept-Encoding} entries.
|
||||
* Use {@code -1} for the default value (100), {@code 0} for no cache.
|
||||
* </dd>
|
||||
* <dt>etags</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to generate ETags in responses.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* <dt>maxCachedFiles</dt>
|
||||
* <dd>
|
||||
* The max number of cached static resources.
|
||||
* Use {@code -1} for the default value (2048) or {@code 0} for no cache.
|
||||
* </dd>
|
||||
* <dt>maxCachedFileSize</dt>
|
||||
* <dd>
|
||||
* The max size in bytes of a single cached static resource.
|
||||
* Use {@code -1} for the default value (128 MiB) or {@code 0} for no cache.
|
||||
* </dd>
|
||||
* <dt>maxCacheSize</dt>
|
||||
* <dd>
|
||||
* The max size in bytes of the cache for static resources.
|
||||
* Use {@code -1} for the default value (256 MiB) or {@code 0} for no cache.
|
||||
* </dd>
|
||||
* <dt>otherGzipFileExtensions</dt>
|
||||
* <dd>
|
||||
* A comma-separated list of extensions of files whose content is implicitly
|
||||
* gzipped.
|
||||
* Defaults to {@code .svgz}.
|
||||
* </dd>
|
||||
* <dt>precompressed</dt>
|
||||
* <dd>
|
||||
* Omitted by default, so that no pre-compressed content will be served.
|
||||
* If set to {@code true}, the default set of pre-compressed formats will be used.
|
||||
* Otherwise can be set to a comma-separated list of {@code encoding=extension} pairs,
|
||||
* such as: {@code br=.br,gzip=.gz,bzip2=.bz}, where {@code encoding} is used as the
|
||||
* value for the {@code Content-Encoding} header.
|
||||
* </dd>
|
||||
* <dt>redirectWelcome</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to redirect welcome files, otherwise they are forwarded.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* <dt>stylesheet</dt>
|
||||
* <dd>
|
||||
* Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
|
||||
* The path of a custom stylesheet to style the directory listing HTML.
|
||||
* </dd>
|
||||
* <dt>useFileMappedBuffer</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to use file mapping to serve static resources.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* <dt>welcomeServlets</dt>
|
||||
* <dd>
|
||||
* Use {@code false} to only serve welcome resources from the file system.
|
||||
* Use {@code true} to dispatch welcome resources to a matching Servlet
|
||||
* (for example mapped to {@code *.welcome}), when the welcome resources
|
||||
* does not exist on file system.
|
||||
* Use {@code exact} to dispatch welcome resource to a Servlet whose mapping
|
||||
* is exactly the same as the welcome resource (for example {@code /index.welcome}),
|
||||
* when the welcome resources does not exist on file system.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* </dl>
|
||||
* <p>The {@code DefaultServlet}, is a specialization of the {@link ResourceServlet} to be mapped to {@code /} as the "default"
|
||||
* servlet for a context.
|
||||
* </p>
|
||||
* <p>
|
||||
* In addition to the servlet init parameters that can be used to configure any {@link ResourceServlet}, the DefaultServlet
|
||||
* also looks at {@link ServletContext#getInitParameter(String)} for any parameter starting with {@link #CONTEXT_INIT}, which
|
||||
* is then stripped and the resulting name interpreted as a {@link ResourceServlet} init parameter.
|
||||
* </p>
|
||||
* <p>
|
||||
* To serve static content other than as the {@code DefaultServlet} mapped to "/", please use the {@link ResourceServlet} directly.
|
||||
* The {@code DefaultServlet} will warn if it is used other than as the default servlet. In future, this may become a fatal error.
|
||||
* </p>
|
||||
*/
|
||||
public class DefaultServlet extends HttpServlet
|
||||
public class DefaultServlet extends ResourceServlet
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultServlet.class);
|
||||
public static final String CONTEXT_INIT = "org.eclipse.jetty.servlet.Default.";
|
||||
|
||||
private ServletContextHandler _contextHandler;
|
||||
private ServletResourceService _resourceService;
|
||||
private WelcomeServletMode _welcomeServletMode;
|
||||
|
||||
public ResourceService getResourceService()
|
||||
{
|
||||
return _resourceService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws ServletException
|
||||
{
|
||||
_contextHandler = initContextHandler(getServletContext());
|
||||
_resourceService = new ServletResourceService(_contextHandler);
|
||||
_resourceService.setWelcomeFactory(_resourceService);
|
||||
Resource baseResource = _contextHandler.getBaseResource();
|
||||
|
||||
String rb = getInitParameter("baseResource", "resourceBase");
|
||||
if (rb != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
baseResource = URIUtil.isRelative(rb) ? _contextHandler.getBaseResource().resolve(rb) : _contextHandler.newResource(rb);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.warn("Unable to create baseResource from {}", rb, e);
|
||||
throw new UnavailableException(e.toString());
|
||||
}
|
||||
}
|
||||
if (baseResource != null && !(baseResource.isDirectory() && baseResource.isReadable()))
|
||||
LOG.warn("baseResource {} is not a readable directory", baseResource);
|
||||
|
||||
List<CompressedContentFormat> precompressedFormats = parsePrecompressedFormats(getInitParameter("precompressed"),
|
||||
getInitBoolean("gzip"), _resourceService.getPrecompressedFormats());
|
||||
|
||||
// Try to get factory from ServletContext attribute.
|
||||
HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName());
|
||||
if (contentFactory == null)
|
||||
{
|
||||
MimeTypes mimeTypes = _contextHandler.getMimeTypes();
|
||||
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes);
|
||||
|
||||
// Use the servers default stylesheet unless there is one explicitly set by an init param.
|
||||
Resource styleSheet = _contextHandler.getServer().getDefaultStyleSheet();
|
||||
String stylesheetParam = getInitParameter("stylesheet");
|
||||
if (stylesheetParam != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpContent styleSheetContent = contentFactory.getContent(stylesheetParam);
|
||||
Resource s = styleSheetContent == null ? null : styleSheetContent.getResource();
|
||||
if (Resources.isReadableFile(s))
|
||||
styleSheet = s;
|
||||
else
|
||||
LOG.warn("Stylesheet {} does not exist", stylesheetParam);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.warn("Unable to use stylesheet: {}", stylesheetParam, e);
|
||||
else
|
||||
LOG.warn("Unable to use stylesheet: {} - {}", stylesheetParam, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (getInitBoolean("useFileMappedBuffer", false))
|
||||
contentFactory = new FileMappingHttpContentFactory(contentFactory);
|
||||
|
||||
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css");
|
||||
contentFactory = new PreCompressedHttpContentFactory(contentFactory, precompressedFormats);
|
||||
|
||||
int maxCacheSize = getInitInt("maxCacheSize", -2);
|
||||
int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
|
||||
int maxCachedFiles = getInitInt("maxCachedFiles", -2);
|
||||
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
|
||||
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
|
||||
{
|
||||
ByteBufferPool bufferPool = getByteBufferPool(_contextHandler);
|
||||
ValidatingCachingHttpContentFactory cached = new ValidatingCachingHttpContentFactory(contentFactory,
|
||||
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
|
||||
contentFactory = cached;
|
||||
if (maxCacheSize >= 0)
|
||||
cached.setMaxCacheSize(maxCacheSize);
|
||||
if (maxCachedFileSize >= 0)
|
||||
cached.setMaxCachedFileSize(maxCachedFileSize);
|
||||
if (maxCachedFiles >= 0)
|
||||
cached.setMaxCachedFiles(maxCachedFiles);
|
||||
}
|
||||
}
|
||||
_resourceService.setHttpContentFactory(contentFactory);
|
||||
|
||||
if (_contextHandler.getWelcomeFiles() == null)
|
||||
_contextHandler.setWelcomeFiles(new String[]{"index.html", "index.jsp"});
|
||||
|
||||
_resourceService.setAcceptRanges(getInitBoolean("acceptRanges", _resourceService.isAcceptRanges()));
|
||||
_resourceService.setDirAllowed(getInitBoolean("dirAllowed", _resourceService.isDirAllowed()));
|
||||
boolean redirectWelcome = getInitBoolean("redirectWelcome", false);
|
||||
_resourceService.setWelcomeMode(redirectWelcome ? ResourceService.WelcomeMode.REDIRECT : ResourceService.WelcomeMode.SERVE);
|
||||
_resourceService.setPrecompressedFormats(precompressedFormats);
|
||||
_resourceService.setEtags(getInitBoolean("etags", _resourceService.isEtags()));
|
||||
|
||||
_welcomeServletMode = WelcomeServletMode.NONE;
|
||||
String welcomeServlets = getInitParameter("welcomeServlets");
|
||||
if (welcomeServlets != null)
|
||||
{
|
||||
welcomeServlets = welcomeServlets.toLowerCase(Locale.ENGLISH);
|
||||
_welcomeServletMode = switch (welcomeServlets)
|
||||
{
|
||||
case "true" -> WelcomeServletMode.MATCH;
|
||||
case "exact" -> WelcomeServletMode.EXACT;
|
||||
default -> WelcomeServletMode.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
int encodingHeaderCacheSize = getInitInt("encodingHeaderCacheSize", -1);
|
||||
if (encodingHeaderCacheSize >= 0)
|
||||
_resourceService.setEncodingCacheSize(encodingHeaderCacheSize);
|
||||
|
||||
String cc = getInitParameter("cacheControl");
|
||||
if (cc != null)
|
||||
_resourceService.setCacheControl(cc);
|
||||
|
||||
List<String> gzipEquivalentFileExtensions = new ArrayList<>();
|
||||
String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
|
||||
if (otherGzipExtensions != null)
|
||||
{
|
||||
//comma separated list
|
||||
StringTokenizer tok = new StringTokenizer(otherGzipExtensions, ",", false);
|
||||
while (tok.hasMoreTokens())
|
||||
{
|
||||
String s = tok.nextToken().trim();
|
||||
gzipEquivalentFileExtensions.add((s.charAt(0) == '.' ? s : "." + s));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// .svgz files are gzipped svg files and must be served with Content-Encoding:gzip
|
||||
gzipEquivalentFileExtensions.add(".svgz");
|
||||
}
|
||||
_resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug(" .baseResource = {}", baseResource);
|
||||
LOG.debug(" .resourceService = {}", _resourceService);
|
||||
LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode);
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBufferPool getByteBufferPool(ContextHandler contextHandler)
|
||||
{
|
||||
if (contextHandler == null)
|
||||
return ByteBufferPool.NON_POOLING;
|
||||
Server server = contextHandler.getServer();
|
||||
if (server == null)
|
||||
return ByteBufferPool.NON_POOLING;
|
||||
return server.getByteBufferPool();
|
||||
}
|
||||
|
||||
private String getInitParameter(String name, String... deprecated)
|
||||
{
|
||||
String value = getInitParameter(name);
|
||||
if (value != null)
|
||||
return value;
|
||||
|
||||
for (String d : deprecated)
|
||||
{
|
||||
value = getInitParameter(d);
|
||||
if (value != null)
|
||||
{
|
||||
LOG.warn("Deprecated {} used instead of {}", d, name);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<CompressedContentFormat> parsePrecompressedFormats(String precompressed, Boolean gzip, List<CompressedContentFormat> dft)
|
||||
{
|
||||
if (precompressed == null && gzip == null)
|
||||
{
|
||||
return dft;
|
||||
}
|
||||
List<CompressedContentFormat> ret = new ArrayList<>();
|
||||
if (precompressed != null && precompressed.indexOf('=') > 0)
|
||||
{
|
||||
for (String pair : precompressed.split(","))
|
||||
{
|
||||
String[] setting = pair.split("=");
|
||||
String encoding = setting[0].trim();
|
||||
String extension = setting[1].trim();
|
||||
ret.add(new CompressedContentFormat(encoding, extension));
|
||||
if (gzip == Boolean.TRUE && !ret.contains(CompressedContentFormat.GZIP))
|
||||
ret.add(CompressedContentFormat.GZIP);
|
||||
}
|
||||
}
|
||||
else if (precompressed != null)
|
||||
{
|
||||
if (Boolean.parseBoolean(precompressed))
|
||||
{
|
||||
ret.add(CompressedContentFormat.BR);
|
||||
ret.add(CompressedContentFormat.GZIP);
|
||||
}
|
||||
}
|
||||
else if (gzip == Boolean.TRUE)
|
||||
{
|
||||
// gzip handling is for backwards compatibility with older Jetty
|
||||
ret.add(CompressedContentFormat.GZIP);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
private final AtomicBoolean warned = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
|
@ -405,519 +67,55 @@ public class DefaultServlet extends HttpServlet
|
|||
return value;
|
||||
}
|
||||
|
||||
private Boolean getInitBoolean(String name)
|
||||
{
|
||||
String value = getInitParameter(name);
|
||||
if (value == null || value.length() == 0)
|
||||
return null;
|
||||
return (value.startsWith("t") ||
|
||||
value.startsWith("T") ||
|
||||
value.startsWith("y") ||
|
||||
value.startsWith("Y") ||
|
||||
value.startsWith("1"));
|
||||
}
|
||||
|
||||
private boolean getInitBoolean(String name, boolean dft)
|
||||
{
|
||||
return Optional.ofNullable(getInitBoolean(name)).orElse(dft);
|
||||
}
|
||||
|
||||
private int getInitInt(String name, int dft)
|
||||
{
|
||||
String value = getInitParameter(name);
|
||||
if (value != null && value.length() > 0)
|
||||
return Integer.parseInt(value);
|
||||
return dft;
|
||||
}
|
||||
|
||||
protected ServletContextHandler initContextHandler(ServletContext servletContext)
|
||||
{
|
||||
if (servletContext instanceof ServletContextHandler.ServletContextApi api)
|
||||
return api.getContext().getServletContextHandler();
|
||||
|
||||
Context context = ContextHandler.getCurrentContext();
|
||||
if (context instanceof ContextHandler.ScopedContext scopedContext)
|
||||
return scopedContext.getContextHandler();
|
||||
|
||||
throw new IllegalArgumentException("The servletContext " + servletContext + " " +
|
||||
servletContext.getClass().getName() + " is not " + ContextHandler.ScopedContext.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
|
||||
public void init() throws ServletException
|
||||
{
|
||||
String includedServletPath = (String)httpServletRequest.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
|
||||
String encodedPathInContext = getEncodedPathInContext(httpServletRequest, includedServletPath);
|
||||
boolean included = includedServletPath != null;
|
||||
if ("true".equalsIgnoreCase(getInitParameter("pathInfoOnly")))
|
||||
LOG.warn("DefaultServlet pathInfoOnly is set to true. Use ResourceServlet instead.");
|
||||
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.
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
throw new FileNotFoundException(encodedPathInContext);
|
||||
}
|
||||
|
||||
// no content
|
||||
httpServletResponse.sendError(404);
|
||||
}
|
||||
else
|
||||
@Override
|
||||
protected String getEncodedPathInContext(HttpServletRequest request, boolean included)
|
||||
{
|
||||
// lookup the core request and response as wrapped by the ServletContextHandler
|
||||
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest);
|
||||
ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse();
|
||||
ServletChannel servletChannel = servletContextRequest.getServletChannel();
|
||||
String deprecatedPath = getEncodedPathInContext(request, (String)(included ? request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH) : null));
|
||||
if (deprecatedPath != null)
|
||||
return deprecatedPath;
|
||||
|
||||
// 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 (request.getHttpServletMapping().getMappingMatch() != MappingMatch.DEFAULT)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Response already committed for {}", coreRequest.getHttpURI());
|
||||
return;
|
||||
if (warned.compareAndSet(false, true))
|
||||
LOG.warn("Incorrect mapping for DefaultServlet at %s. Use ResourceServlet".formatted(request.getHttpServletMapping().getPattern()));
|
||||
return super.getEncodedPathInContext(request, included);
|
||||
}
|
||||
|
||||
// Get the content length before we may wrap the content
|
||||
long contentLength = content.getContentLengthValue();
|
||||
|
||||
// Servlet Filters could be interacting with the Response already.
|
||||
if (useServletResponse)
|
||||
content = new UnknownLengthHttpContent(content);
|
||||
|
||||
// The character encoding may be forced
|
||||
String characterEncoding = servletContextResponse.getRawCharacterEncoding();
|
||||
if (characterEncoding != null)
|
||||
content = new ForcedCharacterEncodingHttpContent(content, characterEncoding);
|
||||
|
||||
// If async is supported and the unwrapped content is larger than an output buffer
|
||||
if (httpServletRequest.isAsyncSupported() &&
|
||||
(contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()))
|
||||
{
|
||||
// send the content asynchronously
|
||||
AsyncContext asyncContext = httpServletRequest.startAsync();
|
||||
Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse);
|
||||
_resourceService.doGet(coreRequest, coreResponse, callback, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// send the content blocking
|
||||
try (Blocker.Callback callback = Blocker.callback())
|
||||
{
|
||||
_resourceService.doGet(coreRequest, coreResponse, callback, content);
|
||||
callback.block();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ServletException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
|
||||
if (included)
|
||||
throw new FileNotFoundException(encodedPathInContext);
|
||||
httpServletResponse.setStatus(404);
|
||||
}
|
||||
{
|
||||
if (request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH) instanceof String servletPath)
|
||||
return URIUtil.encodePath(servletPath);
|
||||
|
||||
// must be an include of a named dispatcher. Just use the whole URI
|
||||
return URIUtil.encodePath(request.getServletPath());
|
||||
}
|
||||
|
||||
if (request instanceof ServletApiRequest apiRequest)
|
||||
// Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
|
||||
return Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
|
||||
|
||||
return URIUtil.encodePath(request.getServletPath());
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath)
|
||||
{
|
||||
if (includedServletPath != null)
|
||||
return encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req)));
|
||||
else if (!isDefaultMapping(req))
|
||||
{
|
||||
//a match via an extension mapping will more than likely
|
||||
//have no path info
|
||||
String path = req.getPathInfo();
|
||||
if (StringUtil.isEmpty(path) &&
|
||||
MappingMatch.EXTENSION.equals(req.getHttpServletMapping().getMappingMatch()))
|
||||
path = req.getServletPath();
|
||||
|
||||
return encodePath(path);
|
||||
}
|
||||
else if (req instanceof ServletApiRequest apiRequest)
|
||||
return Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
|
||||
else
|
||||
return Context.getPathInContext(req.getContextPath(), URIUtil.canonicalPath(req.getRequestURI()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("doHead(req={}, resp={}) (calling doGet())", req, resp);
|
||||
doGet(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
// Always return 405: Method Not Allowed for DefaultServlet
|
||||
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
// override to eliminate TRACE that the default HttpServlet impl adds
|
||||
resp.setHeader("Allow", "GET, HEAD, OPTIONS");
|
||||
}
|
||||
|
||||
private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
|
||||
{
|
||||
private final ServletContextHandler _servletContextHandler;
|
||||
|
||||
private ServletResourceService(ServletContextHandler servletContextHandler)
|
||||
{
|
||||
_servletContextHandler = servletContextHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWelcomeTarget(HttpContent content, Request coreRequest)
|
||||
{
|
||||
String[] welcomes = _servletContextHandler.getWelcomeFiles();
|
||||
if (welcomes == null)
|
||||
return null;
|
||||
String pathInContext = Request.getPathInContext(coreRequest);
|
||||
String welcomeTarget = null;
|
||||
Resource base = content.getResource();
|
||||
if (Resources.isReadableDirectory(base))
|
||||
{
|
||||
for (String welcome : welcomes)
|
||||
{
|
||||
String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
|
||||
|
||||
// If the welcome resource is a file, it has
|
||||
// precedence over resources served by Servlets.
|
||||
Resource welcomePath = content.getResource().resolve(welcome);
|
||||
if (Resources.isReadableFile(welcomePath))
|
||||
return welcomeInContext;
|
||||
|
||||
// Check whether a Servlet may serve the welcome resource.
|
||||
if (_welcomeServletMode != WelcomeServletMode.NONE && welcomeTarget == null)
|
||||
{
|
||||
if (!isDefaultMapping(getServletRequest(coreRequest)) && !isIncluded(getServletRequest(coreRequest)))
|
||||
welcomeTarget = URIUtil.addPaths(getServletRequest(coreRequest).getPathInfo(), welcome);
|
||||
|
||||
ServletHandler.MappedServlet entry = _servletContextHandler.getServletHandler().getMappedServlet(welcomeInContext);
|
||||
// Is there a different Servlet that may serve the welcome resource?
|
||||
if (entry != null && entry.getServletHolder().getServletInstance() != DefaultServlet.this)
|
||||
{
|
||||
if (_welcomeServletMode == WelcomeServletMode.MATCH || entry.getPathSpec().getDeclaration().equals(welcomeInContext))
|
||||
{
|
||||
welcomeTarget = welcomeInContext;
|
||||
// Do not break the loop, because we want to try other welcome resources
|
||||
// that may be files and take precedence over Servlet welcome resources.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return welcomeTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void serveWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
|
||||
{
|
||||
HttpServletRequest servletRequest = getServletRequest(request);
|
||||
HttpServletResponse servletResponse = getServletResponse(response);
|
||||
|
||||
boolean included = isIncluded(servletRequest);
|
||||
|
||||
RequestDispatcher dispatcher = servletRequest.getServletContext().getRequestDispatcher(welcomeTarget);
|
||||
if (dispatcher == null)
|
||||
{
|
||||
// We know that the welcome target exists and can be served.
|
||||
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (included)
|
||||
{
|
||||
dispatcher.include(servletRequest, servletResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
servletRequest.setAttribute("org.eclipse.jetty.server.welcome", welcomeTarget);
|
||||
dispatcher.forward(servletRequest, servletResponse);
|
||||
}
|
||||
callback.succeeded();
|
||||
}
|
||||
catch (ServletException e)
|
||||
{
|
||||
callback.failed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rehandleWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
|
||||
{
|
||||
serveWelcome(request, response, callback, welcomeTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={})", coreRequest, coreResponse, callback, statusCode);
|
||||
writeHttpError(coreRequest, coreResponse, callback, statusCode, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, Throwable cause)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, cause={})", coreRequest, coreResponse, callback, cause, cause);
|
||||
|
||||
int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500;
|
||||
String reason = null;
|
||||
if (cause instanceof HttpException httpException)
|
||||
{
|
||||
statusCode = httpException.getCode();
|
||||
reason = httpException.getReason();
|
||||
}
|
||||
writeHttpError(coreRequest, coreResponse, callback, statusCode, reason, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode, String reason, Throwable cause)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={}, reason={}, cause={})", coreRequest, coreResponse, callback, statusCode, reason, cause, cause);
|
||||
HttpServletRequest request = getServletRequest(coreRequest);
|
||||
HttpServletResponse response = getServletResponse(coreResponse);
|
||||
try
|
||||
{
|
||||
if (isIncluded(request))
|
||||
return;
|
||||
if (cause != null)
|
||||
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause);
|
||||
response.sendError(statusCode, reason);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// TODO: Need a better exception?
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException
|
||||
{
|
||||
boolean included = isIncluded(getServletRequest(request));
|
||||
if (included)
|
||||
return false;
|
||||
return super.passConditionalHeaders(request, response, content, callback);
|
||||
}
|
||||
|
||||
private HttpServletRequest getServletRequest(Request request)
|
||||
{
|
||||
ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class);
|
||||
if (servletCoreRequest != null)
|
||||
return servletCoreRequest.getServletRequest();
|
||||
|
||||
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
|
||||
if (servletContextRequest != null)
|
||||
return servletContextRequest.getServletApiRequest();
|
||||
|
||||
throw new IllegalStateException("instanceof " + request.getClass());
|
||||
}
|
||||
|
||||
private HttpServletResponse getServletResponse(Response response)
|
||||
{
|
||||
ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class);
|
||||
if (servletCoreResponse != null)
|
||||
return servletCoreResponse.getServletResponse();
|
||||
|
||||
ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class);
|
||||
if (servletContextResponse != null)
|
||||
return servletContextResponse.getServletApiResponse();
|
||||
|
||||
throw new IllegalStateException("instanceof " + response.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly)
|
||||
{
|
||||
String servletPath = isPathInfoOnly ? "/" : includedServletPath;
|
||||
String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
|
||||
return URIUtil.addPaths(servletPath, pathInfo);
|
||||
}
|
||||
|
||||
private static boolean isIncluded(HttpServletRequest request)
|
||||
{
|
||||
return request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
|
||||
}
|
||||
|
||||
protected boolean isDefaultMapping(HttpServletRequest req)
|
||||
{
|
||||
if (req.getHttpServletMapping().getMappingMatch() == MappingMatch.DEFAULT)
|
||||
return true;
|
||||
return (req.getDispatcherType() != DispatcherType.REQUEST) && "default".equals(getServletConfig().getServletName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an existing HttpContent with one that takes has an unknown/unspecified length.
|
||||
*/
|
||||
private static class UnknownLengthHttpContent extends HttpContent.Wrapper
|
||||
{
|
||||
public UnknownLengthHttpContent(HttpContent content)
|
||||
{
|
||||
super(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getContentLength()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthValue()
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ForcedCharacterEncodingHttpContent extends HttpContent.Wrapper
|
||||
{
|
||||
private final String characterEncoding;
|
||||
private final String contentType;
|
||||
|
||||
public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding)
|
||||
{
|
||||
super(Objects.requireNonNull(content));
|
||||
this.characterEncoding = characterEncoding;
|
||||
if (content.getContentTypeValue() == null || content.getResource().isDirectory())
|
||||
{
|
||||
this.contentType = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
String mimeType = content.getContentTypeValue();
|
||||
int idx = mimeType.indexOf(";charset");
|
||||
if (idx >= 0)
|
||||
mimeType = mimeType.substring(0, idx);
|
||||
this.contentType = mimeType + ";charset=" + characterEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getContentType()
|
||||
{
|
||||
return new HttpField(HttpHeader.CONTENT_TYPE, this.contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentTypeValue()
|
||||
{
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCharacterEncoding()
|
||||
{
|
||||
return this.characterEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>The different modes a welcome resource may be served by a Servlet.</p>
|
||||
*/
|
||||
private enum WelcomeServletMode
|
||||
{
|
||||
/**
|
||||
* <p>Welcome targets are not served by Servlets.</p>
|
||||
* <p>The welcome target must exist as a file on the filesystem.</p>
|
||||
*/
|
||||
NONE,
|
||||
/**
|
||||
* <p>Welcome target that exist as files on the filesystem are
|
||||
* served, otherwise a matching Servlet may serve the welcome target.</p>
|
||||
*/
|
||||
MATCH,
|
||||
/**
|
||||
* <p>Welcome target that exist as files on the filesystem are
|
||||
* served, otherwise an exact matching Servlet may serve the welcome target.</p>
|
||||
*/
|
||||
EXACT
|
||||
}
|
||||
|
||||
private static class AsyncContextCallback implements Callback
|
||||
{
|
||||
private final AsyncContext _asyncContext;
|
||||
private final HttpServletResponse _response;
|
||||
|
||||
private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response)
|
||||
{
|
||||
_asyncContext = asyncContext;
|
||||
_response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
_asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("AsyncContextCallback failed {}", _asyncContext, x);
|
||||
// It is known that this callback is only failed if the response is already committed,
|
||||
// thus we can only abort the response here.
|
||||
_response.sendError(-1);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ExceptionUtil.addSuppressedIfNotAssociated(x, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_asyncContext.complete();
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Async get failed", x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,6 +258,12 @@ public class Dispatcher implements RequestDispatcher
|
|||
return null;
|
||||
return vals.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getRequest());
|
||||
}
|
||||
}
|
||||
|
||||
private class ForwardRequest extends ParameterRequestWrapper
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), _servletContextRequest);
|
||||
}
|
||||
|
||||
static class AmbiguousURI extends ServletApiRequest
|
||||
{
|
||||
private final String msg;
|
||||
|
|
|
@ -100,7 +100,7 @@ public class ServletCoreRequest implements Request
|
|||
.authority(request.getServerName(), request.getServerPort());
|
||||
|
||||
if (included)
|
||||
builder.path(addEncodedPaths(request.getContextPath(), encodePath(DefaultServlet.getIncludedPathInContext(request, includedServletPath, false))));
|
||||
builder.path(addEncodedPaths(request.getContextPath(), encodePath(ResourceServlet.getIncludedPathInContext(request, includedServletPath))));
|
||||
else if (request.getDispatcherType() != DispatcherType.REQUEST)
|
||||
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
|
||||
else
|
||||
|
|
|
@ -38,6 +38,7 @@ import jakarta.servlet.DispatcherType;
|
|||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
|
@ -52,7 +53,6 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||
import org.eclipse.jetty.http.HttpTester;
|
||||
import org.eclipse.jetty.http.UriCompliance;
|
||||
import org.eclipse.jetty.http.content.ResourceHttpContent;
|
||||
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.IOResources;
|
||||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
|
@ -1383,7 +1383,7 @@ public class DefaultServletTest
|
|||
FS.ensureDirExists(altRoot);
|
||||
|
||||
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/alt/*");
|
||||
defholder.setInitParameter("resourceBase", altRoot.toUri().toASCIIString());
|
||||
defholder.setInitParameter("baseResource", altRoot.toUri().toASCIIString());
|
||||
defholder.setInitParameter("dirAllowed", "false");
|
||||
defholder.setInitParameter("redirectWelcome", "false");
|
||||
defholder.setInitParameter("welcomeServlets", "true");
|
||||
|
@ -1394,7 +1394,8 @@ public class DefaultServletTest
|
|||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String includeTarget = req.getParameter("includeTarget");
|
||||
req.getRequestDispatcher(includeTarget).include(req, resp);
|
||||
RequestDispatcher requestDispatcher = req.getRequestDispatcher(includeTarget);
|
||||
requestDispatcher.include(req, resp);
|
||||
}
|
||||
});
|
||||
context.addServlet(gwholder, "/gateway");
|
||||
|
@ -2001,10 +2002,9 @@ public class DefaultServletTest
|
|||
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(response.getContent(), containsString("<h1>Hello World</h1>"));
|
||||
|
||||
ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
|
||||
|
||||
/*
|
||||
TODO: fix after HttpContent changes.
|
||||
ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
|
||||
HttpContent content = factory.getContent("/index.html", 200);
|
||||
ByteBuffer buffer = content.getDirectBuffer();
|
||||
assertThat("Buffer is direct", buffer.isDirect(), is(true));
|
||||
|
@ -3425,22 +3425,17 @@ public class DefaultServletTest
|
|||
response.setCharacterEncoding("utf-8");
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathInfoOnly() throws Exception
|
||||
public void testNotPathInfoOnly() throws Exception
|
||||
{
|
||||
ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS);
|
||||
context.setWelcomeFiles(new String[]{"index.y", "index.x"});
|
||||
ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
resp.setContentType("text/plain");
|
||||
resp.setCharacterEncoding("UTF-8");
|
||||
|
@ -3456,27 +3451,27 @@ public class DefaultServletTest
|
|||
context.getServletHandler().addServlet(indexServlet);
|
||||
context.getServletHandler().addServletMapping(indexMapping);
|
||||
|
||||
Path pathTest = MavenTestingUtils.getTestResourcePath("pathTest");
|
||||
Path docroot = MavenTestingUtils.getTestResourcePath("docroot");
|
||||
|
||||
Path defaultDir = pathTest.resolve("default");
|
||||
ServletHolder slashHolder = new ServletHolder("default", new DefaultServlet());
|
||||
slashHolder.setInitParameter("redirectWelcome", "false");
|
||||
slashHolder.setInitParameter("welcomeServlets", "true");
|
||||
slashHolder.setInitParameter("baseResource", defaultDir.toAbsolutePath().toString());
|
||||
slashHolder.setInitParameter("baseResource", docroot.toAbsolutePath().toString());
|
||||
context.addServlet(slashHolder, "/");
|
||||
|
||||
Path rDir = pathTest.resolve("rdir");
|
||||
ServletHolder rHolder = new ServletHolder("rdefault", new DefaultServlet());
|
||||
Path altroot = MavenTestingUtils.getTestResourcePath("altroot");
|
||||
ServletHolder rHolder = new ServletHolder("alt", new DefaultServlet());
|
||||
rHolder.setInitParameter("redirectWelcome", "false");
|
||||
rHolder.setInitParameter("welcomeServlets", "true");
|
||||
rHolder.setInitParameter("baseResource", rDir.toAbsolutePath().toString());
|
||||
context.addServlet(rHolder, "/r/*");
|
||||
rHolder.setInitParameter("pathInfoOnly", "false");
|
||||
rHolder.setInitParameter("baseResource", altroot.toAbsolutePath().toString());
|
||||
context.addServlet(rHolder, "/all/*");
|
||||
|
||||
server.stop();
|
||||
server.setHandler(context);
|
||||
server.start();
|
||||
String rawRequest = """
|
||||
GET /c1/r/ HTTP/1.1\r
|
||||
GET /c1/all/index.html HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
Connection: close\r
|
||||
\r
|
||||
|
@ -3484,7 +3479,7 @@ public class DefaultServletTest
|
|||
|
||||
String rawResponse = connector.getResponse(rawRequest);
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
assertThat(response.getContent(), containsString("testPathInfoOnly-OK"));
|
||||
assertThat(response.getContent(), containsString("this is alternate index content"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -3652,11 +3647,6 @@ public class DefaultServletTest
|
|||
response.setCharacterEncoding("utf-8");
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleteFile(Path file) throws IOException
|
||||
|
|
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>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- Reuse Forks causes test failures -->
|
||||
<reuseForks>true</reuseForks>
|
||||
<argLine>@{argLine} ${jetty.surefire.argLine}
|
||||
--add-modules org.eclipse.jetty.util.ajax
|
||||
|
|
|
@ -25,6 +25,7 @@ import jakarta.servlet.DispatcherType;
|
|||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.ee9.nested.ContextHandler;
|
||||
|
@ -125,6 +126,7 @@ public class ContextScopeListenerTest
|
|||
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/initialPath");
|
||||
ContentResponse response = _client.GET(uri);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
|
||||
Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(() -> _history.size() == 7);
|
||||
assertHistory(
|
||||
"enterScope /initialPath",
|
||||
"doGet",
|
||||
|
|
Loading…
Reference in New Issue