diff --git a/pax-web-jetty/src/main/java/org/ops4j/pax/web/service/jetty/internal/web/DefaultServlet.java b/pax-web-jetty/src/main/java/org/ops4j/pax/web/service/jetty/internal/web/DefaultServlet.java index 1f40600ae0..a6f0f6c081 100644 --- a/pax-web-jetty/src/main/java/org/ops4j/pax/web/service/jetty/internal/web/DefaultServlet.java +++ b/pax-web-jetty/src/main/java/org/ops4j/pax/web/service/jetty/internal/web/DefaultServlet.java @@ -1,21 +1,6 @@ -/* - * Copyright 2022 OPS4J. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ // // ======================================================================== -// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// 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 @@ -26,226 +11,300 @@ // ======================================================================== // -package org.ops4j.pax.web.service.jetty.internal.web; +package org.eclipse.jetty.ee10.servlet; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.file.InvalidPathException; +import java.time.Duration; import java.util.ArrayList; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashSet; import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.StringTokenizer; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.UnavailableException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +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.HttpServletResponseWrapper; import org.eclipse.jetty.http.CompressedContentFormat; -import org.eclipse.jetty.http.HttpContent; +import org.eclipse.jetty.http.HttpException; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MimeTypes; -import org.eclipse.jetty.http.PreEncodedHttpField; -import org.eclipse.jetty.http.pathmap.MatchedResource; -import org.eclipse.jetty.server.CachedContentFactory; -import org.eclipse.jetty.server.ResourceContentFactory; +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.ByteBufferInputStream; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.server.Context; +import org.eclipse.jetty.server.HttpStream; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.ResourceService; -import org.eclipse.jetty.server.ResourceService.WelcomeFactory; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ResourceHandler; -import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.util.Blocker; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.util.resource.Resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The default servlet. - *

- * This servlet, normally mapped to /, provides the handling for static - * content, OPTION and TRACE methods for the context. - * The following initParameters are supported, these can be set either - * on the servlet itself or as ServletContext initParameters with a prefix - * of org.eclipse.jetty.servlet.Default. : - *

- *  acceptRanges      If true, range requests and responses are
- *                    supported
- *
- *  dirAllowed        If true, directory listings are returned if no
- *                    welcome file is found. Else 403 Forbidden.
- *
- *  welcomeServlets   If true, attempt to dispatch to welcome files
- *                    that are servlets, but only after no matching static
- *                    resources could be found. If false, then a welcome
- *                    file must exist on disk. If "exact", then exact
- *                    servlet matches are supported without an existing file.
- *                    Default is false.
- *
- *                    This must be false if you want directory listings,
- *                    but have index.jsp in your welcome file list.
- *
- *  redirectWelcome   If true, welcome files are redirected rather than
- *                    forwarded to.
- *
- *  gzip              If set to true, then static content will be served as
- *                    gzip content encoded if a matching resource is
- *                    found ending with ".gz" (default false)
- *                    (deprecated: use precompressed)
- *
- *  precompressed     If set to a comma separated list of encoding types (that may be
- *                    listed in a requests Accept-Encoding header) to file
- *                    extension mappings to look for and serve. For example:
- *                    "br=.br,gzip=.gz,bzip2=.bz".
- *                    If set to a boolean True, then a default set of compressed formats
- *                    will be used, otherwise no precompressed formats.
- *
- *  resourceBase      Set to replace the context resource base
- *
- *  resourceCache     If set, this is a context attribute name, which the servlet
- *                    will use to look for a shared ResourceCache instance.
- *
- *  relativeResourceBase
- *                    Set with a pathname relative to the base of the
- *                    servlet context root. Useful for only serving static content out
- *                    of only specific subdirectories.
- *
- *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
- *
- *  stylesheet        Set with the location of an optional stylesheet that will be used
- *                    to decorate the directory listing html.
- *
- *  etags             If True, weak etags will be generated and handled.
- *
- *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
- *  maxCachedFileSize The maximum size of a file to cache
- *  maxCachedFiles    The maximum number of files to cache
- *
- *  useFileMappedBuffer
- *                    If set to true, it will use mapped file buffer to serve static content
- *                    when using NIO connector. Setting this value to false means that
- *                    a direct buffer will be used instead of a mapped file buffer.
- *                    This is set to false by default by this class, but may be overridden
- *                    by eg webdefault.xml
- *
- *  cacheControl      If set, all static content will have this value set as the cache-control
- *                    header.
- *
- *  otherGzipFileExtensions
- *                    Other file extensions that signify that a file is already compressed. Eg ".svgz"
- *
- *  encodingHeaderCacheSize
- *                    Max entries in a cache of ACCEPT-ENCODING headers.
- * 
- * - *

Pax Web 8: I had to copy this servlet from Jetty code to change some fields from private to protected.

+ *

The default Servlet, normally mapped to {@code /}, that handles static resources.

+ *

The following init parameters are supported:

+ *
+ *
acceptRanges
+ *
+ * Use {@code true} to accept range requests, defaults to {@code true}. + *
+ *
baseResource
+ *
+ * Defaults to the context's baseResource. + * The root directory to look for static resources. + *
+ *
cacheControl
+ *
+ * The value of the {@code Cache-Control} header. + * If omitted, no {@code Cache-Control} header is generated in responses. + * By default is omitted. + *
+ *
cacheValidationTime
+ *
+ * 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. + *
+ *
dirAllowed
+ *
+ * Use {@code true} to serve directory listing if no welcome file is found. + * Otherwise responds with {@code 403 Forbidden}. + * Defaults to {@code true}. + *
+ *
encodingHeaderCacheSize
+ *
+ * Max number of cached {@code Accept-Encoding} entries. + * Use {@code -1} for the default value (100), {@code 0} for no cache. + *
+ *
etags
+ *
+ * Use {@code true} to generate ETags in responses. + * Defaults to {@code false}. + *
+ *
maxCachedFiles
+ *
+ * The max number of cached static resources. + * Use {@code -1} for the default value (2048) or {@code 0} for no cache. + *
+ *
maxCachedFileSize
+ *
+ * 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. + *
+ *
maxCacheSize
+ *
+ * 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. + *
+ *
otherGzipFileExtensions
+ *
+ * A comma-separated list of extensions of files whose content is implicitly + * gzipped. + * Defaults to {@code .svgz}. + *
+ *
pathInfoOnly
+ *
+ * Use {@code true} to use only the request {@code pathInfo} to look for + * static resources. + * Defaults to {@code false}. + *
+ *
precompressed
+ *
+ * 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. + *
+ *
redirectWelcome
+ *
+ * Use {@code true} to redirect welcome files, otherwise they are forwarded. + * Defaults to {@code false}. + *
+ *
stylesheet
+ *
+ * Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}. + * The path of a custom stylesheet to style the directory listing HTML. + *
+ *
useFileMappedBuffer
+ *
+ * Use {@code true} to use file mapping to serve static resources. + * Defaults to {@code false}. + *
+ *
welcomeServlets
+ *
+ * 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}. + *
+ *
*/ -// CHECKSTYLE:OFF -public class DefaultServlet extends HttpServlet implements ResourceFactory, WelcomeFactory +public class DefaultServlet extends HttpServlet { - public static final String CONTEXT_INIT = "org.eclipse.jetty.servlet.Default."; - private static final Logger LOG = LoggerFactory.getLogger(DefaultServlet.class); - private static final long serialVersionUID = 4930458713846881193L; - - protected final ResourceService _resourceService; - private ServletContext _servletContext; - private ContextHandler _contextHandler; - - private boolean _welcomeServlets = false; - private boolean _welcomeExactServlets = false; - - private Resource _resourceBase; - protected CachedContentFactory _cache; - - private MimeTypes _mimeTypes; - protected String[] _welcomes; - private Resource _stylesheet; - private boolean _useFileMappedBuffer = false; - private String _relativeResourceBase; - private ServletHandler _servletHandler; - - public DefaultServlet(ResourceService resourceService) - { - _resourceService = resourceService; - } + private ServletContextHandler _contextHandler; + private ServletResourceService _resourceService; + private WelcomeServletMode _welcomeServletMode; + private Resource _baseResource; + private boolean _isPathInfoOnly; - public DefaultServlet() + public ResourceService getResourceService() { - this(new ResourceService()); + return _resourceService; } @Override - public void init() - throws UnavailableException + public void init() throws ServletException { - _servletContext = getServletContext(); - _contextHandler = initContextHandler(_servletContext); - - _mimeTypes = _contextHandler.getMimeTypes(); - - _welcomes = _contextHandler.getWelcomeFiles(); - if (_welcomes == null) - _welcomes = new String[]{"index.html", "index.jsp"}; + _contextHandler = initContextHandler(getServletContext()); + _resourceService = new ServletResourceService(_contextHandler); + _resourceService.setWelcomeFactory(_resourceService); + _baseResource = _contextHandler.getBaseResource(); - _resourceService.setAcceptRanges(getInitBoolean("acceptRanges", _resourceService.isAcceptRanges())); - _resourceService.setDirAllowed(getInitBoolean("dirAllowed", _resourceService.isDirAllowed())); - _resourceService.setRedirectWelcome(getInitBoolean("redirectWelcome", _resourceService.isRedirectWelcome())); - _resourceService.setPrecompressedFormats(parsePrecompressedFormats(getInitParameter("precompressed"), getInitBoolean("gzip"), _resourceService.getPrecompressedFormats())); - _resourceService.setPathInfoOnly(getInitBoolean("pathInfoOnly", _resourceService.isPathInfoOnly())); - _resourceService.setEtags(getInitBoolean("etags", _resourceService.isEtags())); - - if ("exact".equals(getInitParameter("welcomeServlets"))) - { - _welcomeExactServlets = true; - _welcomeServlets = false; - } - else - _welcomeServlets = getInitBoolean("welcomeServlets", _welcomeServlets); - - _useFileMappedBuffer = getInitBoolean("useFileMappedBuffer", _useFileMappedBuffer); - - _relativeResourceBase = getInitParameter("relativeResourceBase"); - - String rb = getInitParameter("resourceBase"); + String rb = getInitParameter("baseResource", "resourceBase"); if (rb != null) { - if (_relativeResourceBase != null) - throw new UnavailableException("resourceBase & relativeResourceBase"); try { - _resourceBase = _contextHandler.newResource(rb); + _baseResource = Objects.requireNonNull(_contextHandler.newResource(rb)); } catch (Exception e) { - LOG.warn("Unable to create resourceBase from {}", rb, e); + LOG.warn("Unable to create baseResource from {}", rb, e); throw new UnavailableException(e.toString()); } } - String css = getInitParameter("stylesheet"); - try + List 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) { - if (css != null) + MimeTypes mimeTypes = _contextHandler.getMimeTypes(); + ResourceFactory resourceFactory = _baseResource != null ? ResourceFactory.of(_baseResource) : this::getResource; + contentFactory = new ResourceHttpContentFactory(resourceFactory, 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) { - _stylesheet = Resource.newResource(css); - if (!_stylesheet.exists()) + 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) { - LOG.warn("!{}", css); - _stylesheet = null; + if (LOG.isDebugEnabled()) + LOG.warn("Unable to use stylesheet: {}", stylesheetParam, e); + else + LOG.warn("Unable to use stylesheet: {} - {}", stylesheetParam, e.toString()); } } - if (_stylesheet == null) + + 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) { - _stylesheet = ResourceHandler.getDefaultStylesheet(); + 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); } } - catch (Exception e) + _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())); + + _isPathInfoOnly = getInitBoolean("pathInfoOnly", _isPathInfoOnly); + + _welcomeServletMode = WelcomeServletMode.NONE; + String welcomeServlets = getInitParameter("welcomeServlets"); + if (welcomeServlets != null) { - if (LOG.isDebugEnabled()) - LOG.warn("Unable to use stylesheet: {}", css, e); - else - LOG.warn("Unable to use stylesheet: {} - {}", css, e.toString()); + welcomeServlets = welcomeServlets.toLowerCase(Locale.ENGLISH); + _welcomeServletMode = switch (welcomeServlets) + { + case "true" -> WelcomeServletMode.MATCH; + case "exact" -> WelcomeServletMode.EXACT; + default -> WelcomeServletMode.NONE; + }; } int encodingHeaderCacheSize = getInitInt("encodingHeaderCacheSize", -1); @@ -254,50 +313,7 @@ public void init() String cc = getInitParameter("cacheControl"); if (cc != null) - _resourceService.setCacheControl(new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cc)); - - String resourceCache = getInitParameter("resourceCache"); - int maxCacheSize = getInitInt("maxCacheSize", -2); - int maxCachedFileSize = getInitInt("maxCachedFileSize", -2); - int maxCachedFiles = getInitInt("maxCachedFiles", -2); - if (resourceCache != null) - { - if (maxCacheSize != -1 || maxCachedFileSize != -2 || maxCachedFiles != -2) - LOG.debug("ignoring resource cache configuration, using resourceCache attribute"); - if (_relativeResourceBase != null || _resourceBase != null) - throw new UnavailableException("resourceCache specified with resource bases"); - _cache = (CachedContentFactory)_servletContext.getAttribute(resourceCache); - } - - try - { - if (_cache == null && (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2)) - { - _cache = new CachedContentFactory(null, this, _mimeTypes, _useFileMappedBuffer, _resourceService.isEtags(), _resourceService.getPrecompressedFormats()); - if (maxCacheSize >= 0) - _cache.setMaxCacheSize(maxCacheSize); - if (maxCachedFileSize >= -1) - _cache.setMaxCachedFileSize(maxCachedFileSize); - if (maxCachedFiles >= -1) - _cache.setMaxCachedFiles(maxCachedFiles); - _servletContext.setAttribute(resourceCache == null ? "resourceCache" : resourceCache, _cache); - } - } - catch (Exception e) - { - LOG.warn("Unable to setup CachedContentFactory", e); - throw new UnavailableException(e.toString()); - } - - HttpContent.ContentFactory contentFactory = _cache; - if (contentFactory == null) - { - contentFactory = new ResourceContentFactory(this, _mimeTypes, _resourceService.getPrecompressedFormats()); - if (resourceCache != null) - _servletContext.setAttribute(resourceCache, contentFactory); - } - _resourceService.setContentFactory(contentFactory); - _resourceService.setWelcomeFactory(this); + _resourceService.setCacheControl(cc); List gzipEquivalentFileExtensions = new ArrayList<>(); String otherGzipExtensions = getInitParameter("otherGzipFileExtensions"); @@ -313,18 +329,50 @@ public void init() } else { - //.svgz files are gzipped svg files and must be served with Content-Encoding:gzip + // .svgz files are gzipped svg files and must be served with Content-Encoding:gzip gzipEquivalentFileExtensions.add(".svgz"); } _resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions); - _servletHandler = _contextHandler.getChildHandlerByClass(ServletHandler.class); - if (LOG.isDebugEnabled()) - LOG.debug("resource base = {}", _resourceBase); + { + LOG.debug(" .baseResource = {}", _baseResource); + LOG.debug(" .resourceService = {}", _resourceService); + LOG.debug(" .isPathInfoOnly = {}", _isPathInfoOnly); + LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode); + } + } + + private static ByteBufferPool getByteBufferPool(ContextHandler contextHandler) + { + if (contextHandler == null) + return new ByteBufferPool.NonPooling(); + Server server = contextHandler.getServer(); + if (server == null) + return new ByteBufferPool.NonPooling(); + return server.getByteBufferPool(); } - private CompressedContentFormat[] parsePrecompressedFormats(String precompressed, Boolean gzip, CompressedContentFormat[] dft) + private String getInitParameter(String name, String... deprecated) + { + String value = super.getInitParameter(name); + if (value != null) + return value; + + for (String d : deprecated) + { + value = super.getInitParameter(d); + if (value != null) + { + LOG.warn("Deprecated {} used instead of {}", d, name); + return value; + } + } + + return null; + } + + private List parsePrecompressedFormats(String precompressed, Boolean gzip, List dft) { if (precompressed == null && gzip == null) { @@ -356,40 +404,7 @@ else if (gzip == Boolean.TRUE) // gzip handling is for backwards compatibility with older Jetty ret.add(CompressedContentFormat.GZIP); } - return ret.toArray(new CompressedContentFormat[ret.size()]); - } - - /** - * Compute the field _contextHandler.
- * In the case where the DefaultServlet is deployed on the HttpService it is likely that - * this method needs to be overwritten to unwrap the ServletContext facade until we reach - * the original jetty's ContextHandler. - * - * @param servletContext The servletContext of this servlet. - * @return the jetty's ContextHandler for this servletContext. - */ - protected ContextHandler initContextHandler(ServletContext servletContext) - { - ContextHandler.Context scontext = ContextHandler.getCurrentContext(); - if (scontext == null) - { - if (servletContext instanceof ContextHandler.Context) - return ((ContextHandler.Context)servletContext).getContextHandler(); - else - throw new IllegalArgumentException("The servletContext " + servletContext + " " + - servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName()); - } - else - return ContextHandler.getCurrentContext().getContextHandler(); - } - - @Override - public String getInitParameter(String name) - { - String value = getServletContext().getInitParameter(CONTEXT_INIT + name); - if (value == null) - value = super.getInitParameter(name); - return value; + return ret; } private Boolean getInitBoolean(String name) @@ -412,127 +427,876 @@ private boolean getInitBoolean(String name, boolean dft) private int getInitInt(String name, int dft) { String value = getInitParameter(name); - if (value == null) - value = getInitParameter(name); if (value != null && value.length() > 0) return Integer.parseInt(value); return dft; } - /** - * get Resource to serve. - * Map a path to a resource. The default implementation calls - * HttpContext.getResource but derived servlets may provide - * their own mapping. - * - * @param pathInContext The path to find a resource for. - * @return The resource to serve. - */ + 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()); + } + + protected boolean isPathInfoOnly() + { + return _isPathInfoOnly; + } + @Override - public Resource getResource(String pathInContext) + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Resource r = null; - if (_relativeResourceBase != null) - pathInContext = URIUtil.addPaths(_relativeResourceBase, pathInContext); + String includedServletPath = (String)req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + boolean included = includedServletPath != null; + String encodedPathInContext; + if (included) + encodedPathInContext = URIUtil.encodePath(getIncludedPathInContext(req, includedServletPath, isPathInfoOnly())); + else if (isPathInfoOnly()) + encodedPathInContext = URIUtil.encodePath(req.getPathInfo()); + else if (req instanceof ServletApiRequest apiRequest) + encodedPathInContext = Context.getPathInContext(req.getContextPath(), apiRequest.getServletContextRequest().getHttpURI().getCanonicalPath()); + else + encodedPathInContext = Context.getPathInContext(req.getContextPath(), URIUtil.canonicalPath(req.getRequestURI())); + + if (LOG.isDebugEnabled()) + LOG.debug("doGet(req={}, resp={}) pathInContext={}, included={}", req, resp, encodedPathInContext, included); try { - if (_resourceBase != null) + HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(req)); + if (LOG.isDebugEnabled()) + LOG.debug("content = {}", content); + + if (content == null || Resources.missing(content.getResource())) { - r = _resourceBase.addPath(pathInContext); - if (!_contextHandler.checkAlias(pathInContext, r)) - r = null; + 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 + resp.sendError(404); } - else if (_servletContext instanceof ContextHandler.Context) + else { - r = _contextHandler.getResource(pathInContext); + ServletCoreRequest coreRequest = new ServletCoreRequest(req); + ServletCoreResponse coreResponse = new ServletCoreResponse(coreRequest, resp); + + if (coreResponse.isCommitted()) + { + if (LOG.isDebugEnabled()) + LOG.debug("Response already committed for {}", coreRequest.getHttpURI()); + return; + } + + // Servlet Filters could be interacting with the Response already. + if (coreResponse.isHttpServletResponseWrapped() || + coreResponse.isWritingOrStreaming()) + { + content = new UnknownLengthHttpContent(content); + } + + ServletContextResponse contextResponse = coreResponse.getServletContextResponse(); + if (contextResponse != null) + { + String characterEncoding = contextResponse.getRawCharacterEncoding(); + if (characterEncoding != null) + content = new ForcedCharacterEncodingHttpContent(content, characterEncoding); + } + + // serve content + try (Blocker.Callback callback = Blocker.callback()) + { + _resourceService.doGet(coreRequest, coreResponse, callback, content); + callback.block(); + } + catch (Exception e) + { + throw new ServletException(e); + } } - else + } + catch (InvalidPathException e) + { + if (LOG.isDebugEnabled()) + LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e); + if (included) + throw new FileNotFoundException(encodedPathInContext); + resp.setStatus(404); + } + } + + @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); + } + + private Resource getResource(URI uri) + { + String uriPath = uri.getRawPath(); + Resource result = null; + try + { + result = _contextHandler.getResource(uriPath); + } + catch (IOException x) + { + LOG.trace("IGNORED", x); + } + if (LOG.isDebugEnabled()) + LOG.debug("Resource {}={}", uriPath, result); + return result; + } + + private static class ServletCoreRequest extends Request.Wrapper + { + // TODO fully implement this class and move it to the top level + // TODO Some methods are directed to core that probably should be intercepted + + private final HttpServletRequest _servletRequest; + private final HttpFields _httpFields; + private final HttpURI _uri; + + ServletCoreRequest(HttpServletRequest request) + { + super(ServletContextRequest.getServletContextRequest(request)); + _servletRequest = request; + + HttpFields.Mutable fields = HttpFields.build(); + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { - return null; + String headerName = headerNames.nextElement(); + Enumeration headerValues = request.getHeaders(headerName); + while (headerValues.hasMoreElements()) + { + String headerValue = headerValues.nextElement(); + fields.add(new HttpField(headerName, headerValue)); + } } - if (LOG.isDebugEnabled()) - LOG.debug("Resource {}={}", pathInContext, r); + _httpFields = fields.asImmutable(); + String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + boolean included = includedServletPath != null; + if (request.getDispatcherType() == DispatcherType.REQUEST) + _uri = getWrapped().getHttpURI(); + else if (included) + _uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(getIncludedPathInContext(request, includedServletPath, false))); + else + _uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))); } - catch (IOException e) + + @Override + public HttpFields getHeaders() { - LOG.trace("IGNORED", e); + return _httpFields; } - if ((r == null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css")) - r = _stylesheet; + @Override + public HttpURI getHttpURI() + { + return _uri; + } - return r; + @Override + public String getId() + { + return _servletRequest.getRequestId(); + } + + @Override + public String getMethod() + { + return _servletRequest.getMethod(); + } + + @Override + public boolean isSecure() + { + return _servletRequest.isSecure(); + } + + @Override + public boolean addErrorListener(Predicate onError) + { + return false; + } + + @Override + public void addHttpStreamWrapper(Function wrapper) + { + } + + @Override + public Object removeAttribute(String name) + { + Object value = _servletRequest.getAttribute(name); + _servletRequest.removeAttribute(name); + return value; + } + + @Override + public Object setAttribute(String name, Object attribute) + { + Object value = _servletRequest.getAttribute(name); + _servletRequest.setAttribute(name, attribute); + return value; + } + + @Override + public Object getAttribute(String name) + { + return _servletRequest.getAttribute(name); + } + + @Override + public Set getAttributeNameSet() + { + Set set = new HashSet<>(); + Enumeration e = _servletRequest.getAttributeNames(); + while (e.hasMoreElements()) + set.add(e.nextElement()); + return set; + } + + @Override + public void clearAttributes() + { + Enumeration e = _servletRequest.getAttributeNames(); + while (e.hasMoreElements()) + _servletRequest.removeAttribute(e.nextElement()); + } } - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException + private static class HttpServletResponseHttpFields implements HttpFields.Mutable { - if (!_resourceService.doGet(request, response)) - response.sendError(404); + private final HttpServletResponse _response; + + private HttpServletResponseHttpFields(HttpServletResponse response) + { + _response = response; + } + + @Override + public ListIterator listIterator() + { + // The minimum requirement is to implement the listIterator, but it is inefficient. + // Other methods are implemented for efficiency. + final ListIterator list = _response.getHeaderNames().stream() + .map(n -> new HttpField(n, _response.getHeader(n))) + .collect(Collectors.toList()) + .listIterator(); + + return new ListIterator<>() + { + HttpField _last; + + @Override + public boolean hasNext() + { + return list.hasNext(); + } + + @Override + public HttpField next() + { + return _last = list.next(); + } + + @Override + public boolean hasPrevious() + { + return list.hasPrevious(); + } + + @Override + public HttpField previous() + { + return _last = list.previous(); + } + + @Override + public int nextIndex() + { + return list.nextIndex(); + } + + @Override + public int previousIndex() + { + return list.previousIndex(); + } + + @Override + public void remove() + { + if (_last != null) + { + // This is not exactly the right semantic for repeated field names + list.remove(); + _response.setHeader(_last.getName(), null); + } + } + + @Override + public void set(HttpField httpField) + { + list.set(httpField); + _response.setHeader(httpField.getName(), httpField.getValue()); + } + + @Override + public void add(HttpField httpField) + { + list.add(httpField); + _response.addHeader(httpField.getName(), httpField.getValue()); + } + }; + } + + @Override + public Mutable add(String name, String value) + { + _response.addHeader(name, value); + return this; + } + + @Override + public Mutable add(HttpHeader header, HttpHeaderValue value) + { + _response.addHeader(header.asString(), value.asString()); + return this; + } + + @Override + public Mutable add(HttpHeader header, String value) + { + _response.addHeader(header.asString(), value); + return this; + } + + @Override + public Mutable add(HttpField field) + { + _response.addHeader(field.getName(), field.getValue()); + return this; + } + + @Override + public Mutable put(HttpField field) + { + _response.setHeader(field.getName(), field.getValue()); + return this; + } + + @Override + public Mutable put(String name, String value) + { + _response.setHeader(name, value); + return this; + } + + @Override + public Mutable put(HttpHeader header, HttpHeaderValue value) + { + _response.setHeader(header.asString(), value.asString()); + return this; + } + + @Override + public Mutable put(HttpHeader header, String value) + { + _response.setHeader(header.asString(), value); + return this; + } + + @Override + public Mutable put(String name, List list) + { + Objects.requireNonNull(name); + Objects.requireNonNull(list); + boolean first = true; + for (String s : list) + { + if (first) + _response.setHeader(name, s); + else + _response.addHeader(name, s); + first = false; + } + return this; + } + + @Override + public Mutable remove(HttpHeader header) + { + _response.setHeader(header.asString(), null); + return this; + } + + @Override + public Mutable remove(EnumSet fields) + { + for (HttpHeader header : fields) + remove(header); + return this; + } + + @Override + public Mutable remove(String name) + { + _response.setHeader(name, null); + return this; + } } - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException + private static class ServletCoreResponse implements Response { - doGet(request, response); + // TODO fully implement this class and move it to the top level + + private final HttpServletResponse _response; + private final ServletCoreRequest _coreRequest; + private final Response _coreResponse; + private final HttpFields.Mutable _httpFields; + + public ServletCoreResponse(ServletCoreRequest coreRequest, HttpServletResponse response) + { + _coreRequest = coreRequest; + _response = response; + _coreResponse = ServletContextResponse.getServletContextResponse(response); + _httpFields = new HttpServletResponseHttpFields(response); + } + + @Override + public HttpFields.Mutable getHeaders() + { + return _httpFields; + } + + public ServletContextResponse getServletContextResponse() + { + if (_response instanceof ServletApiResponse) + { + ServletApiResponse apiResponse = (ServletApiResponse)_response; + return apiResponse.getResponse(); + } + return null; + } + + @Override + public boolean isCommitted() + { + return _response.isCommitted(); + } + + /** + * Test if the HttpServletResponse is wrapped by the webapp. + * + * @return true if wrapped. + */ + public boolean isHttpServletResponseWrapped() + { + return (_response instanceof HttpServletResponseWrapper); + } + + /** + * Test if {@link HttpServletResponse#getOutputStream()} or + * {@link HttpServletResponse#getWriter()} has been called already + * + * @return true if {@link HttpServletResponse} has started to write or stream content + */ + public boolean isWritingOrStreaming() + { + ServletContextResponse servletContextResponse = Response.as(_coreResponse, ServletContextResponse.class); + return servletContextResponse.isWritingOrStreaming(); + } + + public boolean isWriting() + { + ServletContextResponse servletContextResponse = Response.as(_coreResponse, ServletContextResponse.class); + return servletContextResponse.isWriting(); + } + + @Override + public void write(boolean last, ByteBuffer byteBuffer, Callback callback) + { + try + { + if (BufferUtil.hasContent(byteBuffer)) + { + if (isWriting()) + { + String characterEncoding = _response.getCharacterEncoding(); + try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer); + InputStreamReader reader = new InputStreamReader(bbis, characterEncoding)) + { + IO.copy(reader, _response.getWriter()); + } + + if (last) + _response.getWriter().close(); + } + else + { + BufferUtil.writeTo(byteBuffer, _response.getOutputStream()); + if (last) + _response.getOutputStream().close(); + } + } + + callback.succeeded(); + } + catch (Throwable t) + { + callback.failed(t); + } + } + + @Override + public Request getRequest() + { + return _coreRequest; + } + + @Override + public int getStatus() + { + return _response.getStatus(); + } + + @Override + public void setStatus(int code) + { + if (LOG.isDebugEnabled()) + LOG.debug("{}.setStatus({})", this.getClass().getSimpleName(), code); + _response.setStatus(code); + } + + @Override + public Supplier getTrailersSupplier() + { + return null; + } + + @Override + public void setTrailersSupplier(Supplier trailers) + { + } + + @Override + public boolean isCompletedSuccessfully() + { + return _coreResponse.isCompletedSuccessfully(); + } + + @Override + public void reset() + { + _response.reset(); + } + + @Override + public CompletableFuture writeInterim(int status, HttpFields headers) + { + return null; + } } - @Override - protected void doHead(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException + private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory { - doGet(request, response); + private final ServletContextHandler _servletContextHandler; + + private ServletResourceService(ServletContextHandler servletContextHandler) + { + _servletContextHandler = servletContextHandler; + } + + @Override + public String getWelcomeTarget(Request coreRequest) + { + String[] welcomes = _servletContextHandler.getWelcomeFiles(); + if (welcomes == null) + return null; + + HttpServletRequest request = getServletRequest(coreRequest); + String pathInContext = Request.getPathInContext(coreRequest); + String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + String requestTarget; + if (includedServletPath != null) + requestTarget = getIncludedPathInContext(request, includedServletPath, isPathInfoOnly()); + else + requestTarget = isPathInfoOnly() ? request.getPathInfo() : pathInContext; + + String welcomeTarget = null; + Resource base = _baseResource.resolve(requestTarget); + 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 = base.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() != 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 redirectWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException + { + HttpServletRequest servletRequest = getServletRequest(request); + HttpServletResponse servletResponse = getServletResponse(response); + + boolean included = isIncluded(servletRequest); + + String servletPath = included ? (String)servletRequest.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) + : servletRequest.getServletPath(); + + if (isPathInfoOnly()) + welcomeTarget = URIUtil.addPaths(servletPath, welcomeTarget); + + servletResponse.setContentLength(0); + Response.sendRedirect(request, response, callback, 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 + { + // TODO: not sure if this is allowed here. + 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) + { + // TODO, this unwrapping is fragile + return ((ServletCoreRequest)request)._servletRequest; + } + + private HttpServletResponse getServletResponse(Response response) + { + // TODO, this unwrapping is fragile + return ((ServletCoreResponse)response)._response; + } } - @Override - protected void doTrace(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException + private static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly) { - response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + String servletPath = isPathInfoOnly ? "/" : includedServletPath; + String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + return URIUtil.addPaths(servletPath, pathInfo); } - @Override - protected void doOptions(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException + private static boolean isIncluded(HttpServletRequest request) { - response.setHeader("Allow", "GET,HEAD,POST,OPTIONS"); + return request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; } - @Override - public void destroy() + /** + * Wrap an existing HttpContent with one that takes has an unknown/unspecified length. + */ + private static class UnknownLengthHttpContent extends HttpContent.Wrapper { - if (_cache != null) - _cache.flushCache(); - super.destroy(); + public UnknownLengthHttpContent(HttpContent content) + { + super(content); + } + + @Override + public HttpField getContentLength() + { + return null; + } + + @Override + public long getContentLengthValue() + { + return -1; + } } - @Override - public String getWelcomeFile(String pathInContext) + private static class ForcedCharacterEncodingHttpContent extends HttpContent.Wrapper { - if (_welcomes == null) - return null; + private final String characterEncoding; + private final String contentType; - String welcomeServlet = null; - for (String s : _welcomes) + public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding) { - String welcomeInContext = URIUtil.addPaths(pathInContext, s); - Resource welcome = getResource(welcomeInContext); - if (welcome != null && welcome.exists()) - return welcomeInContext; + super(content); + this.characterEncoding = characterEncoding; + String mimeType = content.getContentTypeValue(); + int idx = mimeType.indexOf(";charset"); + if (idx >= 0) + mimeType = mimeType.substring(0, idx); + this.contentType = mimeType + ";charset=" + this.characterEncoding; + } - if ((_welcomeServlets || _welcomeExactServlets) && welcomeServlet == null) - { - MatchedResource entry = _servletHandler.getMatchedServlet(welcomeInContext); - if (entry != null && entry.getResource().getServletHolder().getServletInstance() != this && - (_welcomeServlets || (_welcomeExactServlets && entry.getPathSpec().getDeclaration().equals(welcomeInContext)))) - welcomeServlet = welcomeInContext; - } + @Override + public HttpField getContentType() + { + return new HttpField(HttpHeader.CONTENT_TYPE, this.contentType); } - return welcomeServlet; + + @Override + public String getContentTypeValue() + { + return this.contentType; + } + + @Override + public String getCharacterEncoding() + { + return this.characterEncoding; + } + } + + /** + *

The different modes a welcome resource may be served by a Servlet.

+ */ + private enum WelcomeServletMode + { + /** + *

Welcome targets are not served by Servlets.

+ *

The welcome target must exist as a file on the filesystem.

+ */ + NONE, + /** + *

Welcome target that exist as files on the filesystem are + * served, otherwise a matching Servlet may serve the welcome target.

+ */ + MATCH, + /** + *

Welcome target that exist as files on the filesystem are + * served, otherwise an exact matching Servlet may serve the welcome target.

+ */ + EXACT } } -// CHECKSTYLE:ON