diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java index be4ee311610..31355a0c8d6 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.util.HashMap; import java.util.Map; @@ -180,7 +179,7 @@ public class CachingContentFactory implements HttpContent.ContentFactory } HttpContent httpContent = _authority.getContent(path, maxBuffer); // Do not cache directories or files that are too big - if (httpContent != null && !Files.isDirectory(httpContent.getPath()) && httpContent.getContentLengthValue() <= _maxCachedFileSize) + if (httpContent != null && !httpContent.getResource().isDirectory() && httpContent.getContentLengthValue() <= _maxCachedFileSize) { httpContent = cachingHttpContent = new CachingHttpContent(path, null, httpContent); _cache.put(path, cachingHttpContent); @@ -210,14 +209,14 @@ public class CachingContentFactory implements HttpContent.ContentFactory if (_useFileMappedBuffer) { // mmap the content into memory - byteBuffer = BufferUtil.toMappedBuffer(httpContent.getPath(), 0, _contentLengthValue); + byteBuffer = BufferUtil.toMappedBuffer(httpContent.getResource().getPath(), 0, _contentLengthValue); } else { // TODO use pool & check length limit // load the content into memory byteBuffer = ByteBuffer.allocateDirect((int)_contentLengthValue); - try (SeekableByteChannel channel = Files.newByteChannel(httpContent.getPath())) + try (SeekableByteChannel channel = Files.newByteChannel(httpContent.getResource().getPath())) { // fill buffer int read = 0; @@ -257,7 +256,7 @@ public class CachingContentFactory implements HttpContent.ContentFactory _cacheKey = key; _buffer = byteBuffer; - _lastModifiedValue = Files.getLastModifiedTime(httpContent.getPath()); + _lastModifiedValue = Files.getLastModifiedTime(httpContent.getResource().getPath()); _delegate = httpContent; _lastAccessed = System.nanoTime(); } @@ -289,7 +288,7 @@ public class CachingContentFactory implements HttpContent.ContentFactory { try { - FileTime lastModifiedTime = Files.getLastModifiedTime(_delegate.getPath()); + FileTime lastModifiedTime = Files.getLastModifiedTime(_delegate.getResource().getPath()); if (lastModifiedTime.equals(_lastModifiedValue)) { _lastAccessed = System.nanoTime(); @@ -386,12 +385,6 @@ public class CachingContentFactory implements HttpContent.ContentFactory return _delegate.getETagValue(); } - @Override - public Path getPath() - { - return _delegate.getPath(); - } - @Override public Resource getResource() { diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java index 26ad161a7cf..72cac66d3ed 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java @@ -31,8 +31,6 @@ import org.eclipse.jetty.util.resource.Resource; * reuse in from a cache). *

*/ -// TODO also review metadata (like getContentLengthValue and getLastModifiedValue) to check if they can be removed as those -// are available via the Path API public interface HttpContent { HttpField getContentType(); @@ -59,10 +57,6 @@ public interface HttpContent String getETagValue(); - // TODO rename? - Path getPath(); - - // TODO getPath() is supposed to replace the following Resource getResource(); Map getPrecompressedContents(); diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java index 5f4f5121b59..1edad1805a7 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/PrecompressedHttpContent.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.http; import java.nio.ByteBuffer; -import java.nio.file.Path; import java.util.Map; import org.eclipse.jetty.http.MimeTypes.Type; @@ -37,12 +36,6 @@ public class PrecompressedHttpContent implements HttpContent } } - @Override - public Path getPath() - { - return _content.getPath(); - } - @Override public Resource getResource() { @@ -128,8 +121,7 @@ public class PrecompressedHttpContent implements HttpContent return String.format("%s@%x{e=%s,r=%s|%s,lm=%s|%s,ct=%s}", this.getClass().getSimpleName(), hashCode(), _format, - _content.getPath(), _precompressedContent.getPath(), -// _content.getResource().lastModified(), _precompressedContent.getResource().lastModified(), + _content.getResource().lastModified(), _precompressedContent.getResource().lastModified(), 0L, 0L, getContentType()); } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java index 071af363bbf..83bffd1bcb4 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java @@ -141,12 +141,6 @@ public class ResourceHttpContent implements HttpContent return _resource.length(); } - @Override - public Path getPath() - { - return _path; - } - @Override public Resource getResource() { diff --git a/jetty-core/jetty-server/src/main/config/etc/well-known.xml b/jetty-core/jetty-server/src/main/config/etc/well-known.xml index 7f59e3f1168..8266a365df6 100644 --- a/jetty-core/jetty-server/src/main/config/etc/well-known.xml +++ b/jetty-core/jetty-server/src/main/config/etc/well-known.xml @@ -7,7 +7,7 @@ - + diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java index 3cda6541a50..66ac72fb3d3 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CachedContentFactory.java @@ -16,7 +16,6 @@ package org.eclipse.jetty.server; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -271,7 +270,7 @@ public class CachedContentFactory implements HttpContent.ContentFactory { String compressedPathInContext = pathInContext + format.getExtension(); CachedHttpContent compressedContent = _cache.get(compressedPathInContext); - if (compressedContent != null && compressedContent.isValid() && Files.getLastModifiedTime(compressedContent.getPath()).toMillis() >= resource.lastModified()) + if (compressedContent != null && compressedContent.isValid() && Files.getLastModifiedTime(compressedContent.getResource().getPath()).toMillis() >= resource.lastModified()) compressedContents.put(format, compressedContent); // Is there a precompressed resource? @@ -441,12 +440,6 @@ public class CachedContentFactory implements HttpContent.ContentFactory return _key != null; } - @Override - public Path getPath() - { - return _resource.getPath(); - } - @Override public Resource getResource() { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java index e093cd199db..eeecb3f3900 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java @@ -14,15 +14,31 @@ package org.eclipse.jetty.server; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jetty.http.CompressedContentFormat; +import org.eclipse.jetty.http.DateParser; import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpField; 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.PreEncodedHttpField; +import org.eclipse.jetty.http.QuotedCSV; +import org.eclipse.jetty.http.QuotedQualityCSV; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,28 +46,39 @@ import org.slf4j.LoggerFactory; import static java.util.Arrays.stream; /** - * Abstract resource service, used by DefaultServlet and ResourceHandler + * Resource service, used by DefaultServlet and ResourceHandler */ -//TODO remove public class ResourceService { private static final Logger LOG = LoggerFactory.getLogger(ResourceService.class); - private static final PreEncodedHttpField ACCEPT_RANGES = new PreEncodedHttpField(HttpHeader.ACCEPT_RANGES, "bytes"); + private static final int NO_CONTENT_LENGTH = -1; + private static final int USE_KNOWN_CONTENT_LENGTH = -2; - private HttpContent.ContentFactory _contentFactory; - private WelcomeFactory _welcomeFactory; - private boolean _acceptRanges = true; - private boolean _dirAllowed = true; - private boolean _redirectWelcome = false; - private CompressedContentFormat[] _precompressedFormats = new CompressedContentFormat[0]; - private String[] _preferredEncodingOrder = new String[0]; - private final Map> _preferredEncodingOrderCache = new ConcurrentHashMap<>(); - private int _encodingCacheSize = 100; + private Resource _defaultStylesheet; + private Resource _stylesheet; private boolean _pathInfoOnly = false; + private CompressedContentFormat[] _precompressedFormats = new CompressedContentFormat[0]; + private WelcomeFactory _welcomeFactory; + private boolean _redirectWelcome = false; private boolean _etags = false; - private HttpField _cacheControl; private List _gzipEquivalentFileExtensions; + private HttpContent.ContentFactory _contentFactory; + private final Map> _preferredEncodingOrderCache = new ConcurrentHashMap<>(); + private String[] _preferredEncodingOrder = new String[0]; + private int _encodingCacheSize = 100; + private boolean _dirAllowed = true; + private boolean _acceptRanges = true; + private HttpField _cacheControl; + + public ResourceService() + { + } + + public HttpContent getContent(String servletPath, int outputBufferSize) throws IOException + { + return _contentFactory.getContent(servletPath, outputBufferSize); + } public HttpContent.ContentFactory getContentFactory() { @@ -63,192 +90,91 @@ public class ResourceService _contentFactory = contentFactory; } - public WelcomeFactory getWelcomeFactory() + /** + * @return the cacheControl header to set on all static content. + */ + public String getCacheControl() { - return _welcomeFactory; - } - - public void setWelcomeFactory(WelcomeFactory welcomeFactory) - { - _welcomeFactory = welcomeFactory; - } - - public boolean isAcceptRanges() - { - return _acceptRanges; - } - - public void setAcceptRanges(boolean acceptRanges) - { - _acceptRanges = acceptRanges; - } - - public boolean isDirAllowed() - { - return _dirAllowed; - } - - public void setDirAllowed(boolean dirAllowed) - { - _dirAllowed = dirAllowed; - } - - public boolean isRedirectWelcome() - { - return _redirectWelcome; - } - - public void setRedirectWelcome(boolean redirectWelcome) - { - _redirectWelcome = redirectWelcome; - } - - public CompressedContentFormat[] getPrecompressedFormats() - { - return _precompressedFormats; - } - - public void setPrecompressedFormats(CompressedContentFormat[] precompressedFormats) - { - _precompressedFormats = precompressedFormats; - _preferredEncodingOrder = stream(_precompressedFormats).map(f -> f.getEncoding()).toArray(String[]::new); - } - - public void setEncodingCacheSize(int encodingCacheSize) - { - _encodingCacheSize = encodingCacheSize; - } - - public int getEncodingCacheSize() - { - return _encodingCacheSize; - } - - public boolean isPathInfoOnly() - { - return _pathInfoOnly; - } - - public void setPathInfoOnly(boolean pathInfoOnly) - { - _pathInfoOnly = pathInfoOnly; - } - - public boolean isEtags() - { - return _etags; - } - - public void setEtags(boolean etags) - { - _etags = etags; - } - - public HttpField getCacheControl() - { - return _cacheControl; - } - - public void setCacheControl(HttpField cacheControl) - { - if (cacheControl == null) - _cacheControl = null; - if (cacheControl.getHeader() != HttpHeader.CACHE_CONTROL) - throw new IllegalArgumentException("!Cache-Control"); - _cacheControl = cacheControl instanceof PreEncodedHttpField - ? cacheControl - : new PreEncodedHttpField(cacheControl.getHeader(), cacheControl.getValue()); + return _cacheControl.getValue(); } + /** + * @return file extensions that signify that a file is gzip compressed. Eg ".svgz" + */ public List getGzipEquivalentFileExtensions() { return _gzipEquivalentFileExtensions; } - public void setGzipEquivalentFileExtensions(List gzipEquivalentFileExtensions) + /** + * @return Returns the stylesheet as a Resource. + */ + public Resource getStylesheet() { - _gzipEquivalentFileExtensions = gzipEquivalentFileExtensions; - } - - /* TODO - public boolean doGet(Request request, Response response) throws IOException - { - String servletPath = null; - String pathInfo = null; - Enumeration reqRanges = null; - boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; - if (included) + if (_stylesheet != null) { - servletPath = _pathInfoOnly ? "/" : (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); - pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); - if (servletPath == null) - { - servletPath = request.getServletPath(); - pathInfo = request.getPathInfo(); - } + return _stylesheet; } else { - servletPath = _pathInfoOnly ? "/" : request.getServletPath(); - pathInfo = request.getPathInfo(); - - // Is this a Range request? - reqRanges = request.getHeaders(HttpHeader.RANGE.asString()); - if (!hasDefinedRange(reqRanges)) - reqRanges = null; + if (_defaultStylesheet == null) + { + _defaultStylesheet = getDefaultStylesheet(); + } + return _defaultStylesheet; } + } - String pathInContext = URIUtil.addPaths(servletPath, pathInfo); + public static Resource getDefaultStylesheet() + { + // TODO the returned path should point to the classpath. + // This points to a non-existent file '/jetty-dir.css'. + return Resource.newResource(Path.of("/jetty-dir.css")); + } - boolean endsWithSlash = (pathInfo == null ? (_pathInfoOnly ? "" : servletPath) : pathInfo).endsWith(URIUtil.SLASH); - boolean checkPrecompressedVariants = _precompressedFormats.length > 0 && !endsWithSlash && !included && reqRanges == null; + public void doGet(GenericRequest request, GenericResponse response, Callback callback, HttpContent content) throws Exception + { + String pathInContext = request.getPathInContext(); + + // Is this a Range request? + Enumeration reqRanges = request.getHeaderValues(HttpHeader.RANGE.asString()); + if (!hasDefinedRange(reqRanges)) + reqRanges = null; + + boolean endsWithSlash = pathInContext.endsWith(URIUtil.SLASH); + boolean checkPrecompressedVariants = _precompressedFormats.length > 0 && !endsWithSlash && reqRanges == null; - HttpContent content = null; - boolean releaseContent = true; try { - // Find the content - content = _contentFactory.getContent(pathInContext, response.getBufferSize()); - if (LOG.isDebugEnabled()) - LOG.debug("content={}", content); - - // Not found? - if (content == null || !content.getResource().exists()) - { - if (included) - throw new FileNotFoundException("!" + pathInContext); - notFound(request, response); - return response.isCommitted(); - } - // Directory? if (content.getResource().isDirectory()) { - sendWelcome(content, pathInContext, endsWithSlash, included, request, response); - return true; + sendWelcome(content, pathInContext, endsWithSlash, request, response, callback); + return; } // Strip slash? - if (!included && endsWithSlash && pathInContext.length() > 1) + if (endsWithSlash && pathInContext.length() > 1) { - String q = request.getQueryString(); + // TODO need helper code to edit URIs + String q = request.getHttpURI().getQuery(); pathInContext = pathInContext.substring(0, pathInContext.length() - 1); if (q != null && q.length() != 0) pathInContext += "?" + q; - response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), pathInContext))); - return true; + response.sendRedirect(callback, URIUtil.addPaths(request.getContextPath(), pathInContext)); + return; } // Conditional response? - if (!included && !passConditionalHeaders(request, response, content)) - return true; + if (passConditionalHeaders(request, response, content, callback)) + return; // Precompressed variant available? Map precompressedContents = checkPrecompressedVariants ? content.getPrecompressedContents() : null; if (precompressedContents != null && precompressedContents.size() > 0) { // Tell caches that response may vary by accept-encoding - response.addHeader(HttpHeader.VARY.asString(), HttpHeader.ACCEPT_ENCODING.asString()); + response.putHeader(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING.asString()); List preferredEncodings = getPreferredEncodingOrder(request); CompressedContentFormat precompressedContentEncoding = getBestPrecompressedContent(preferredEncodings, precompressedContents.keySet()); @@ -258,50 +184,37 @@ public class ResourceService if (LOG.isDebugEnabled()) LOG.debug("precompressed={}", precompressedContent); content = precompressedContent; - response.getHeaders().put(HttpHeader.CONTENT_ENCODING, precompressedContentEncoding.getEncoding()); + response.putHeader(HttpHeader.CONTENT_ENCODING, precompressedContentEncoding.getEncoding()); } } // TODO this should be done by HttpContent#getContentEncoding if (isGzippedContent(pathInContext)) - response.getHeaders().put(HttpHeader.CONTENT_ENCODING, "gzip"); + response.putHeader(HttpHeader.CONTENT_ENCODING, "gzip"); // Send the data - releaseContent = sendData(request, response, included, content, reqRanges); + sendData(request, response, callback, content, reqRanges); } // Can be thrown from contentFactory.getContent() call when using invalid characters catch (InvalidPathException e) { if (LOG.isDebugEnabled()) LOG.debug("InvalidPathException for pathInContext: {}", pathInContext, e); - if (included) - throw new FileNotFoundException("!" + pathInContext); - notFound(request, response); - return response.isCommitted(); + response.writeError(callback, HttpStatus.NOT_FOUND_404); } catch (IllegalArgumentException e) { LOG.warn("Failed to serve resource: {}", pathInContext, e); if (!response.isCommitted()) - response.sendError(500, e.getMessage()); + response.writeError(callback, HttpStatus.INTERNAL_SERVER_ERROR_500); } - finally - { - if (releaseContent) - { - if (content != null) - content.release(); - } - } - - return true; } - private List getPreferredEncodingOrder(HttpServletRequest request) + private List getPreferredEncodingOrder(GenericRequest request) { - Enumeration headers = request.getHeaders(HttpHeader.ACCEPT_ENCODING.asString()); + Enumeration headers = request.getHeaderValues(HttpHeader.ACCEPT_ENCODING.asString()); if (!headers.hasMoreElements()) - return emptyList(); + return Collections.emptyList(); String key = headers.nextElement(); if (headers.hasMoreElements()) @@ -332,7 +245,20 @@ public class ResourceService return values; } - private CompressedContentFormat getBestPrecompressedContent(List preferredEncodings, Collection availableFormats) + private boolean isGzippedContent(String path) + { + if (path == null || _gzipEquivalentFileExtensions == null) + return false; + + for (String suffix : _gzipEquivalentFileExtensions) + { + if (path.endsWith(suffix)) + return true; + } + return false; + } + + private CompressedContentFormat getBestPrecompressedContent(List preferredEncodings, java.util.Collection availableFormats) { if (availableFormats.isEmpty()) return null; @@ -348,117 +274,16 @@ public class ResourceService if ("*".equals(encoding)) return availableFormats.iterator().next(); - if (IDENTITY.asString().equals(encoding)) + if (HttpHeaderValue.IDENTITY.asString().equals(encoding)) return null; } return null; } - protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, boolean included, HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException - { - // Redirect to directory - if (!endsWithSlash) - { - StringBuilder buf = new StringBuilder(request.getRequestURI()); - int param = buf.lastIndexOf(";"); - if (param < 0 || buf.lastIndexOf("/", param) > 0) - buf.append('/'); - else - buf.insert(param, '/'); - String q = request.getQueryString(); - if (q != null && q.length() != 0) - { - buf.append('?'); - buf.append(q); - } - response.setContentLength(0); - response.sendRedirect(response.encodeRedirectURL(buf.toString())); - return; - } - - // look for a welcome file - String welcome = _welcomeFactory == null ? null : _welcomeFactory.getWelcomeFile(pathInContext); - - if (welcome != null) - { - String servletPath = included ? (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) - : request.getServletPath(); - - if (_pathInfoOnly) - welcome = URIUtil.addPaths(servletPath, welcome); - - if (LOG.isDebugEnabled()) - LOG.debug("welcome={}", welcome); - - ServletContext context = request.getServletContext(); - - if (_redirectWelcome || context == null) - { - // Redirect to the index - response.setContentLength(0); - - String uri = URIUtil.encodePath(URIUtil.addPaths(request.getContextPath(), welcome)); - String q = request.getQueryString(); - if (q != null && !q.isEmpty()) - uri += "?" + q; - - response.sendRedirect(response.encodeRedirectURL(uri)); - return; - } - - RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome)); - if (dispatcher != null) - { - // Forward to the index - if (included) - dispatcher.include(request, response); - else - { - request.setAttribute("org.eclipse.jetty.server.welcome", welcome); - dispatcher.forward(request, response); - } - } - return; - } - - if (included || passConditionalHeaders(request, response, content)) - sendDirectory(request, response, content.getResource(), pathInContext); - } - - protected boolean isGzippedContent(String path) - { - if (path == null || _gzipEquivalentFileExtensions == null) - return false; - - for (String suffix : _gzipEquivalentFileExtensions) - { - if (path.endsWith(suffix)) - return true; - } - return false; - } - - private boolean hasDefinedRange(Enumeration reqRanges) - { - return (reqRanges != null && reqRanges.hasMoreElements()); - } - - protected void notFound(HttpServletRequest request, HttpServletResponse response) throws IOException - { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } - - protected void sendStatus(HttpServletResponse response, int status, Supplier etag) throws IOException - { - response.setStatus(status); - if (_etags && etag != null) - response.getHeaders().put(HttpHeader.ETAG, etag.get()); - response.flushBuffer(); - } - - protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, HttpContent content) - throws IOException + /** + * @return true if the request was processed, false otherwise. + */ + protected boolean passConditionalHeaders(GenericRequest request, GenericResponse response, HttpContent content, Callback callback) throws IOException { try { @@ -467,39 +292,23 @@ public class ResourceService String ifms = null; long ifums = -1; - if (request instanceof Request) + // Find multiple fields by iteration as an optimization + for (HttpField field : request.getHeaders()) { - // Find multiple fields by iteration as an optimization - for (HttpField field : ((Request)request).getHttpFields()) + if (field.getHeader() != null) { - if (field.getHeader() != null) + switch (field.getHeader()) { - switch (field.getHeader()) + case IF_MATCH -> ifm = field.getValue(); + case IF_NONE_MATCH -> ifnm = field.getValue(); + case IF_MODIFIED_SINCE -> ifms = field.getValue(); + case IF_UNMODIFIED_SINCE -> ifums = DateParser.parseDate(field.getValue()); + default -> { - case IF_MATCH: - ifm = field.getValue(); - break; - case IF_NONE_MATCH: - ifnm = field.getValue(); - break; - case IF_MODIFIED_SINCE: - ifms = field.getValue(); - break; - case IF_UNMODIFIED_SINCE: - ifums = DateParser.parseDate(field.getValue()); - break; - default: } } } } - else - { - ifm = request.getHeader(HttpHeader.IF_MATCH.asString()); - ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString()); - ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString()); - ifums = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString()); - } if (_etags) { @@ -507,7 +316,7 @@ public class ResourceService if (ifm != null) { boolean match = false; - if (etag != null) + if (etag != null && !etag.startsWith("W/")) { QuotedCSV quoted = new QuotedCSV(true, ifm); for (String etagWithSuffix : quoted) @@ -522,8 +331,8 @@ public class ResourceService if (!match) { - sendStatus(response, HttpServletResponse.SC_PRECONDITION_FAILED, null); - return false; + response.writeError(callback, HttpStatus.PRECONDITION_FAILED_412); + return true; } } @@ -532,8 +341,8 @@ public class ResourceService // Handle special case of exact match OR gzip exact match if (CompressedContentFormat.tagEquals(etag, ifnm) && ifnm.indexOf(',') < 0) { - sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, ifnm::toString); - return false; + response.writeError(callback, HttpStatus.NOT_MODIFIED_304); + return true; } // Handle list of tags @@ -542,13 +351,13 @@ public class ResourceService { if (CompressedContentFormat.tagEquals(etag, tag)) { - sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, tag::toString); - return false; + response.writeError(callback, HttpStatus.NOT_MODIFIED_304); + return true; } } // If etag requires content to be served, then do not check if-modified-since - return true; + return false; } } @@ -559,171 +368,158 @@ public class ResourceService String mdlm = content.getLastModifiedValue(); if (ifms.equals(mdlm)) { - sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, content::getETagValue); - return false; + response.writeError(callback, HttpStatus.NOT_MODIFIED_304); + return true; } - long ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString()); - if (ifmsl != -1 && content.getResource().lastModified() / 1000 <= ifmsl / 1000) + long ifmsl = request.getHeaderDate(HttpHeader.IF_MODIFIED_SINCE.asString()); + if (ifmsl != -1 && Files.getLastModifiedTime(content.getResource().getPath()).toMillis() / 1000 <= ifmsl / 1000) { - sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, content::getETagValue); - return false; + response.writeError(callback, HttpStatus.NOT_MODIFIED_304); + return true; } } // Parse the if[un]modified dates and compare to resource - if (ifums != -1 && content.getResource().lastModified() / 1000 > ifums / 1000) + if (ifums != -1 && Files.getLastModifiedTime(content.getResource().getPath()).toMillis() / 1000 > ifums / 1000) { - response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); - return false; + response.writeError(callback, HttpStatus.PRECONDITION_FAILED_412); + return true; } } catch (IllegalArgumentException iae) { if (!response.isCommitted()) - response.sendError(400, iae.getMessage()); + response.writeError(callback, HttpStatus.BAD_REQUEST_400); throw iae; } - return true; + return false; } - protected void sendDirectory(HttpServletRequest request, - HttpServletResponse response, - Resource resource, - String pathInContext) - throws IOException + protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, GenericRequest request, GenericResponse response, Callback callback) throws Exception + { + // Redirect to directory + if (!endsWithSlash) + { + // TODO need helper code to edit URIs + StringBuilder buf = new StringBuilder(request.getHttpURI().asString()); + int param = buf.lastIndexOf(";"); + if (param < 0 || buf.lastIndexOf("/", param) > 0) + buf.append('/'); + else + buf.insert(param, '/'); + String q = request.getHttpURI().getQuery(); + if (q != null && q.length() != 0) + { + buf.append('?'); + buf.append(q); + } + response.putHeaderLong(HttpHeader.CONTENT_LENGTH, 0); + response.sendRedirect(callback, buf.toString()); + return; + } + + // look for a welcome file + if (welcome(request, response, callback)) + return; + + if (!passConditionalHeaders(request, response, content, callback)) + sendDirectory(request, response, content, callback, pathInContext); + } + + protected boolean welcome(GenericRequest request, GenericResponse response, Callback callback) throws IOException + { + String pathInContext = request.getPathInContext(); + String welcome = _welcomeFactory == null ? null : _welcomeFactory.getWelcomeFile(pathInContext); + if (welcome != null) + { + String contextPath = request.getContextPath(); + + if (_pathInfoOnly) + welcome = URIUtil.addPaths(contextPath, welcome); + + if (LOG.isDebugEnabled()) + LOG.debug("welcome={}", welcome); + + if (_redirectWelcome) + { + // Redirect to the index + response.putHeaderLong(HttpHeader.CONTENT_LENGTH, 0); + + // TODO need helper code to edit URIs + String uri = URIUtil.encodePath(URIUtil.addPaths(request.getContextPath(), welcome)); + String q = request.getHttpURI().getQuery(); + if (q != null && !q.isEmpty()) + uri += "?" + q; + + response.sendRedirect(callback, uri); + return true; + } + + // Serve welcome file + HttpContent c = _contentFactory.getContent(welcome, response.getOutputBufferSize()); + sendData(request, response, callback, c, null); + return true; + } + return false; + } + + private void sendDirectory(GenericRequest request, GenericResponse response, HttpContent httpContent, Callback callback, String pathInContext) throws IOException { if (!_dirAllowed) { - response.sendError(HttpServletResponse.SC_FORBIDDEN); + response.writeError(callback, HttpStatus.FORBIDDEN_403); return; } - byte[] data = null; - String base = URIUtil.addEncodedPaths(request.getRequestURI(), URIUtil.SLASH); - String dir = resource.getListHTML(base, pathInContext.length() > 1, request.getQueryString()); + String base = URIUtil.addEncodedPaths(request.getHttpURI().getPath(), URIUtil.SLASH); + String dir = httpContent.getResource().getListHTML(base, pathInContext.length() > 1, request.getHttpURI().getQuery()); if (dir == null) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, - "No directory"); + response.writeError(callback, HttpStatus.FORBIDDEN_403); return; } - data = dir.getBytes(StandardCharsets.UTF_8); - response.setContentType("text/html;charset=utf-8"); - response.setContentLength(data.length); - response.write(true, callback, ByteBuffer.wrap(data)); + byte[] data = dir.getBytes(StandardCharsets.UTF_8); + response.putHeader(HttpHeader.CONTENT_TYPE, "text/html;charset=utf-8"); + response.putHeaderLong(HttpHeader.CONTENT_LENGTH, data.length); + response.writeLast(ByteBuffer.wrap(data), callback); } - protected boolean sendData(HttpServletRequest request, - HttpServletResponse response, - boolean include, - final HttpContent content, - Enumeration reqRanges) - throws IOException + private boolean sendData(GenericRequest request, GenericResponse response, Callback callback, HttpContent content, Enumeration reqRanges) throws IOException { - final long content_length = content.getContentLengthValue(); - - // Get the output stream (or writer) - OutputStream out; - boolean written; - try - { - out = response.getOutputStream(); - - // has something already written to the response? - written = out instanceof HttpOutput - ? ((HttpOutput)out).isWritten() - : true; - } - catch (IllegalStateException e) - { - out = new WriterOutputStream(response.getWriter()); - written = true; // there may be data in writer buffer, so assume written - } + long contentLength = content.getContentLengthValue(); if (LOG.isDebugEnabled()) - LOG.debug(String.format("sendData content=%s out=%s async=%b", content, out, request.isAsyncSupported())); + LOG.debug(String.format("sendData content=%s", content)); - if (reqRanges == null || !reqRanges.hasMoreElements() || content_length < 0) + if (reqRanges == null || !reqRanges.hasMoreElements() || contentLength < 0) { - // if there were no ranges, send entire entity - if (include) - { - // write without headers - writeContent(content, out, 0, content_length); - } - // else if we can't do a bypass write because of wrapping - else if (written) - { - // write normally - putHeaders(response, content, Response.NO_CONTENT_LENGTH); - writeContent(content, out, 0, content_length); - } - // else do a bypass write - else - { - // write the headers - putHeaders(response, content, Response.USE_KNOWN_CONTENT_LENGTH); + // if there were no ranges, send entire entity - // write the content asynchronously if supported - if (request.isAsyncSupported()) - { - final AsyncContext context = request.startAsync(); - context.setTimeout(0); + // write the headers + putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH); - ((HttpOutput)out).sendContent(content, new Callback() - { - @Override - public void succeeded() - { - context.complete(); - content.release(); - } - - @Override - public void failed(Throwable x) - { - String msg = "Failed to send content"; - if (x instanceof IOException) - LOG.debug(msg, x); - else - LOG.warn(msg, x); - context.complete(); - content.release(); - } - - @Override - public InvocationType getInvocationType() - { - return InvocationType.NON_BLOCKING; - } - - @Override - public String toString() - { - return String.format("ResourceService@%x$CB", ResourceService.this.hashCode()); - } - }); - return false; - } - // otherwise write content blocking - ((HttpOutput)out).sendContent(content); - } + // write the content + response.write(content, callback); } else { + throw new UnsupportedOperationException("TODO ranges not yet supported"); + // TODO rewrite with ByteChannel only which should simplify HttpContentRangeWriter as HttpContent's Path always provides a SeekableByteChannel + // but MultiPartOutputStream also needs to be rewritten. +/* // Parse the satisfiable ranges - List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length); + List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, contentLength); // if there are no satisfiable ranges, send 416 response if (ranges == null || ranges.size() == 0) { - putHeaders(response, content, Response.USE_KNOWN_CONTENT_LENGTH); + putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH); response.getHeaders().put(HttpHeader.CONTENT_RANGE, - InclusiveByteRange.to416HeaderRangeString(content_length)); - sendStatus(response, HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, null); + InclusiveByteRange.to416HeaderRangeString(contentLength)); + sendStatus(416, response, callback); return true; } @@ -734,11 +530,11 @@ public class ResourceService InclusiveByteRange singleSatisfiableRange = ranges.iterator().next(); long singleLength = singleSatisfiableRange.getSize(); putHeaders(response, content, singleLength); - response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - if (!response.containsHeader(HttpHeader.DATE.asString())) - response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis()); + response.setStatus(206); + if (!response.getHeaders().contains(HttpHeader.DATE.asString())) + response.getHeaders().addDateField(HttpHeader.DATE.asString(), System.currentTimeMillis()); response.getHeaders().put(HttpHeader.CONTENT_RANGE, - singleSatisfiableRange.toHeaderRangeString(content_length)); + singleSatisfiableRange.toHeaderRangeString(contentLength)); writeContent(content, out, singleSatisfiableRange.getFirst(), singleLength); return true; } @@ -747,19 +543,19 @@ public class ResourceService // 216 response which does not require an overall // content-length header // - putHeaders(response, content, Response.NO_CONTENT_LENGTH); + putHeaders(response, content, NO_CONTENT_LENGTH); String mimetype = content.getContentTypeValue(); if (mimetype == null) - LOG.warn("Unknown mimetype for {}", request.getRequestURI()); - response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - if (!response.containsHeader(HttpHeader.DATE.asString())) - response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis()); + LOG.warn("Unknown mimetype for {}", request.getHttpURI()); + response.setStatus(206); + if (!response.getHeaders().contains(HttpHeader.DATE.asString())) + response.getHeaders().addDateField(HttpHeader.DATE.asString(), System.currentTimeMillis()); // If the request has a "Request-Range" header then we need to // send an old style multipart/x-byteranges Content-Type. This // keeps Netscape and acrobat happy. This is what Apache does. String ctp; - if (request.getHeader(HttpHeader.REQUEST_RANGE.asString()) != null) + if (request.getHeaders().get(HttpHeader.REQUEST_RANGE.asString()) != null) ctp = "multipart/x-byteranges; boundary="; else ctp = "multipart/byteranges; boundary="; @@ -776,7 +572,7 @@ public class ResourceService final int FIELD_SEP = ": ".length(); for (InclusiveByteRange ibr : ranges) { - header[i] = ibr.toHeaderRangeString(content_length); + header[i] = ibr.toHeaderRangeString(contentLength); if (i > 0) // in-part length += CRLF; length += DASHDASH + BOUNDARY + CRLF; @@ -802,61 +598,227 @@ public class ResourceService } multi.close(); + */ } return true; } - private static void writeContent(HttpContent content, OutputStream out, long start, long contentLength) throws IOException + private void putHeaders(GenericResponse response, HttpContent content, long contentLength) { - // Is the write for the whole content? - if (start == 0 && content.getResource().length() == contentLength) - { - // attempt efficient ByteBuffer based write for whole content - ByteBuffer buffer = content.getIndirectBuffer(); - if (buffer != null) - { - BufferUtil.writeTo(buffer, out); - return; - } + // TODO it is very inefficient to do many put's to a HttpFields, as each put is a full iteration. + // it might be better remove headers en masse and then just add the extras: +// headers.remove(EnumSet.of( +// HttpHeader.LAST_MODIFIED, +// HttpHeader.CONTENT_LENGTH, +// HttpHeader.CONTENT_TYPE, +// HttpHeader.CONTENT_ENCODING, +// HttpHeader.ETAG, +// HttpHeader.ACCEPT_RANGES, +// HttpHeader.CACHE_CONTROL +// )); +// HttpField lm = content.getLastModified(); +// if (lm != null) +// headers.add(lm); +// etc. - try (InputStream input = content.getResource().getInputStream()) - { - IO.copy(input, out); - return; - } + HttpField lm = content.getLastModified(); + if (lm != null) + response.putHeader(lm); + + if (contentLength == USE_KNOWN_CONTENT_LENGTH) + { + response.putHeader(content.getContentLength()); + } + else if (contentLength > NO_CONTENT_LENGTH) + { + response.putHeaderLong(HttpHeader.CONTENT_LENGTH, contentLength); } - // Use a ranged writer - try (InputStreamRangeWriter rangeWriter = new InputStreamRangeWriter(() -> content.getInputStream())) + HttpField ct = content.getContentType(); + if (ct != null) + response.putHeader(ct); + + HttpField ce = content.getContentEncoding(); + if (ce != null) + response.putHeader(ce); + + if (_etags) { - rangeWriter.writeTo(out, start, contentLength); + HttpField et = content.getETag(); + if (et != null) + response.putHeader(et); } + + if (_acceptRanges && !response.containsHeader(HttpHeader.ACCEPT_RANGES)) + response.putHeader(new PreEncodedHttpField(HttpHeader.ACCEPT_RANGES, "bytes")); + if (_cacheControl != null && !response.containsHeader(HttpHeader.CACHE_CONTROL)) + response.putHeader(_cacheControl); } - protected void putHeaders(HttpServletResponse response, HttpContent content, long contentLength) + private boolean hasDefinedRange(Enumeration reqRanges) { - if (response instanceof Response) - { - Response r = (Response)response; - r.putHeaders(content, contentLength, _etags); - HttpFields.Mutable fields = r.getHttpFields(); - if (_acceptRanges && !fields.contains(HttpHeader.ACCEPT_RANGES)) - fields.add(ACCEPT_RANGES); - if (_cacheControl != null && !fields.contains(HttpHeader.CACHE_CONTROL)) - fields.add(_cacheControl); - } - else - { - Response.putHeaders(response, content, contentLength, _etags); - if (_acceptRanges && !response.containsHeader(HttpHeader.ACCEPT_RANGES.asString())) - response.getHeaders().put(ACCEPT_RANGES.getName(), ACCEPT_RANGES.getValue()); - - if (_cacheControl != null && !response.containsHeader(HttpHeader.CACHE_CONTROL.asString())) - response.getHeaders().put(_cacheControl.getName(), _cacheControl.getValue()); - } + return (reqRanges != null && reqRanges.hasMoreElements()); } - */ + /** + * @return If true, range requests and responses are supported + */ + public boolean isAcceptRanges() + { + return _acceptRanges; + } + + /** + * @return If true, directory listings are returned if no welcome file is found. Else 403 Forbidden. + */ + public boolean isDirAllowed() + { + return _dirAllowed; + } + + /** + * @return True if ETag processing is done + */ + public boolean isEtags() + { + return _etags; + } + + /** + * @return Precompressed resources formats that can be used to serve compressed variant of resources. + */ + public CompressedContentFormat[] getPrecompressedFormats() + { + return _precompressedFormats; + } + + /** + * @return true, only the path info will be applied to the resourceBase + */ + public boolean isPathInfoOnly() + { + return _pathInfoOnly; + } + + /** + * @return If true, welcome files are redirected rather than forwarded to. + */ + public boolean isRedirectWelcome() + { + return _redirectWelcome; + } + + public WelcomeFactory getWelcomeFactory() + { + return _welcomeFactory; + } + + /** + * @param acceptRanges If true, range requests and responses are supported + */ + public void setAcceptRanges(boolean acceptRanges) + { + _acceptRanges = acceptRanges; + } + + /** + * @param cacheControl the cacheControl header to set on all static content. + */ + public void setCacheControl(String cacheControl) + { + _cacheControl = new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cacheControl); + } + + /** + * @param dirAllowed If true, directory listings are returned if no welcome file is found. Else 403 Forbidden. + */ + public void setDirAllowed(boolean dirAllowed) + { + _dirAllowed = dirAllowed; + } + + /** + * @param etags True if ETag processing is done + */ + public void setEtags(boolean etags) + { + _etags = etags; + } + + /** + * @param gzipEquivalentFileExtensions file extensions that signify that a file is gzip compressed. Eg ".svgz" + */ + public void setGzipEquivalentFileExtensions(List gzipEquivalentFileExtensions) + { + _gzipEquivalentFileExtensions = gzipEquivalentFileExtensions; + } + + /** + * @param precompressedFormats The list of precompresed formats to serve in encoded format if matching resource found. + * For example serve gzip encoded file if ".gz" suffixed resource is found. + */ + public void setPrecompressedFormats(CompressedContentFormat[] precompressedFormats) + { + _precompressedFormats = precompressedFormats; + _preferredEncodingOrder = stream(_precompressedFormats).map(CompressedContentFormat::getEncoding).toArray(String[]::new); + } + + public void setEncodingCacheSize(int encodingCacheSize) + { + _encodingCacheSize = encodingCacheSize; + if (encodingCacheSize > _preferredEncodingOrderCache.size()) + _preferredEncodingOrderCache.clear(); + } + + public int getEncodingCacheSize() + { + return _encodingCacheSize; + } + + /** + * @param pathInfoOnly true, only the path info will be applied to the resourceBase + */ + public void setPathInfoOnly(boolean pathInfoOnly) + { + _pathInfoOnly = pathInfoOnly; + } + + /** + * @param redirectWelcome If true, welcome files are redirected rather than forwarded to. + * redirection is always used if the ResourceHandler is not scoped by + * a ContextHandler + */ + public void setRedirectWelcome(boolean redirectWelcome) + { + _redirectWelcome = redirectWelcome; + } + + public void setWelcomeFactory(WelcomeFactory welcomeFactory) + { + _welcomeFactory = welcomeFactory; + } + + /** + * @param stylesheet The location of the stylesheet to be used as a String. + */ + // TODO accept a Resource instead of a String? + public void setStylesheet(String stylesheet) + { + try + { + _stylesheet = Resource.newResource(Path.of(stylesheet)); + if (!_stylesheet.exists()) + { + LOG.warn("unable to find custom stylesheet: {}", stylesheet); + _stylesheet = null; + } + } + catch (Exception e) + { + LOG.warn("Invalid StyleSheet reference: {}", stylesheet, e); + throw new IllegalArgumentException(stylesheet); + } + } public interface WelcomeFactory { @@ -869,4 +831,44 @@ public class ResourceService */ String getWelcomeFile(String pathInContext) throws IOException; } + + public interface GenericRequest + { + Collection getHeaders(); + + Enumeration getHeaderValues(String name); + + long getHeaderDate(String name); + + HttpURI getHttpURI(); + + String getPathInContext(); + + String getContextPath(); + } + + public interface GenericResponse + { + boolean containsHeader(HttpHeader header); + + void putHeader(HttpField header); + + void putHeader(HttpHeader header, String value); + + void putHeaderLong(HttpHeader name, long value); + + boolean isCommitted(); + + int getOutputBufferSize(); + + boolean isUseOutputDirectByteBuffers(); + + void sendRedirect(Callback callback, String uri); + + void writeError(Callback callback, int status); + + void write(HttpContent content, Callback callback); + + void writeLast(ByteBuffer byteBuffer, Callback callback); + } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java index 1abad45cf48..08f35f1591c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java @@ -16,60 +16,34 @@ package org.eclipse.jetty.server.handler; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; -import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryStream; import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.text.DateFormat; import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.Date; import java.util.Enumeration; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.Set; import org.eclipse.jetty.http.CachingContentFactory; import org.eclipse.jetty.http.CompressedContentFormat; -import org.eclipse.jetty.http.DateGenerator; -import org.eclipse.jetty.http.DateParser; import org.eclipse.jetty.http.HttpContent; 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.HttpMethod; -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.QuotedCSV; -import org.eclipse.jetty.http.QuotedQualityCSV; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.ResourceContentFactory; +import org.eclipse.jetty.server.ResourceService; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; -import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; -import org.eclipse.jetty.util.UrlEncoded; -import org.eclipse.jetty.util.resource.PathCollators; -import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static java.util.Arrays.stream; /** * Resource Handler. @@ -82,126 +56,67 @@ import static java.util.Arrays.stream; * * Missing: * - current context' mime types - * - getContent in HttpContent should go - * - Default stylesheet (needs Path impl for classpath resources) + * - Default stylesheet (needs Resource impl for classpath resources) * - request ranges * - a way to configure caching or not */ public class ResourceHandler extends Handler.Wrapper { - private static final Logger LOG = LoggerFactory.getLogger(ResourceHandler.class); + private final ResourceService _resourceService; - private static final int NO_CONTENT_LENGTH = -1; - private static final int USE_KNOWN_CONTENT_LENGTH = -2; - - private ContextHandler _context; - private Path _defaultStylesheet; + private Resource _resourceBase; private MimeTypes _mimeTypes; - private Path _stylesheet; private List _welcomes = List.of("index.html"); - private Path _baseResource; - private boolean _pathInfoOnly = false; - private CompressedContentFormat[] _precompressedFormats = new CompressedContentFormat[0]; - private Welcomer _welcomer; - private boolean _redirectWelcome = false; - private boolean _etags = false; - private List _gzipEquivalentFileExtensions; - private HttpContent.ContentFactory _contentFactory; - private final Map> _preferredEncodingOrderCache = new ConcurrentHashMap<>(); - private String[] _preferredEncodingOrder = new String[0]; - private int _encodingCacheSize = 100; - private boolean _dirAllowed = true; - private boolean _acceptRanges = true; - private HttpField _cacheControl; public ResourceHandler() { + _resourceService = new ResourceService(); } @Override public void doStart() throws Exception { - Context context = ContextHandler.getCurrentContext(); - if (_baseResource == null && context.getBaseResource() != null) - _baseResource = context.getBaseResource().getPath(); + if (_resourceBase == null) + { + Context context = ContextHandler.getCurrentContext(); + if (context != null) + _resourceBase = context.getBaseResource(); + } -// TODO _context = (context == null ? null : context.getContextHandler()); -// if (_mimeTypes == null) +// TODO // _mimeTypes = _context == null ? new MimeTypes() : _context.getMimeTypes(); + if (_mimeTypes == null) + _mimeTypes = new MimeTypes(); - _mimeTypes = new MimeTypes(); - //_contentFactory = new PathContentFactory(); - // TODO make caching configurable and disabled by default - _contentFactory = new CachingContentFactory(new PathContentFactory()); - _welcomer = new DefaultWelcomer(); + setupContentFactory(); super.doStart(); } - // for testing only - HttpContent.ContentFactory getContentFactory() + private void setupContentFactory() { - return _contentFactory; - } - - /** - * @return Returns the resourceBase. - */ - public Path getBaseResource() - { - return _baseResource; - } - - /** - * @return the cacheControl header to set on all static content. - */ - public String getCacheControl() - { - return _cacheControl.getValue(); - } - - /** - * @return file extensions that signify that a file is gzip compressed. Eg ".svgz" - */ - public List getGzipEquivalentFileExtensions() - { - return _gzipEquivalentFileExtensions; - } - - public MimeTypes getMimeTypes() - { - return _mimeTypes; - } - - /** - * @return Returns the stylesheet as a Resource. - */ - public Path getStylesheet() - { - if (_stylesheet != null) + HttpContent.ContentFactory contentFactory = new CachingContentFactory(new ResourceContentFactory(_resourceBase, _mimeTypes, _resourceService.getPrecompressedFormats())); + _resourceService.setContentFactory(contentFactory); + _resourceService.setWelcomeFactory(pathInContext -> { - return _stylesheet; - } - else - { - if (_defaultStylesheet == null) + if (_welcomes == null) + return null; + + for (String welcome : _welcomes) { - _defaultStylesheet = getDefaultStylesheet(); + // TODO GW: This logic needs to be extensible so that a welcome file may be a servlet (yeah I know it shouldn't + // be called a welcome file then. So for example if /foo/index.jsp is the welcome file, we can't + // serve it's contents - rather we have to let the servlet layer to either a redirect or a RequestDispatcher to it. + // Worse yet, if there was a servlet mapped to /foo/index.html, then we need to be able to dispatch to it + // EVEN IF the file does not exist. + String welcomeInContext = URIUtil.addPaths(pathInContext, welcome); + Resource welcomePath = _resourceBase.resolve(pathInContext).resolve(welcome); + if (welcomePath != null && welcomePath.exists()) + return welcomeInContext; } - return _defaultStylesheet; - } - } - - public static Path getDefaultStylesheet() - { - // TODO the returned path should point to the classpath. - // This points to a non-existent file '/jetty-dir.css'. - return Path.of("/jetty-dir.css"); - } - - public List getWelcomeFiles() - { - return _welcomes; + // not found + return null; + }); } @Override @@ -213,7 +128,7 @@ public class ResourceHandler extends Handler.Wrapper return super.handle(request); } - HttpContent content = _contentFactory.getContent(request.getPathInContext(), request.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()); + HttpContent content = _resourceService.getContent(request.getPathInContext(), request.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()); if (content == null) { // no content - try other handlers @@ -223,764 +138,56 @@ public class ResourceHandler extends Handler.Wrapper { // TODO is it possible to get rid of the lambda allocation? // TODO GW: perhaps HttpContent can extend Request.Processor? - return (rq, rs, cb) -> doGet(rq, rs, cb, content); + return (rq, rs, cb) -> _resourceService.doGet(new ResourceHandlerGenericRequest(rq), new ResourceHandlerGenericResponse(rs), cb, content); } } - private void doGet(Request request, Response response, Callback callback, HttpContent content) throws Exception + // for testing only + HttpContent.ContentFactory getContentFactory() { - String pathInContext = request.getPathInContext(); - - // Is this a Range request? - Enumeration reqRanges = request.getHeaders().getValues(HttpHeader.RANGE.asString()); - if (!hasDefinedRange(reqRanges)) - reqRanges = null; - - - boolean endsWithSlash = pathInContext.endsWith(URIUtil.SLASH); - boolean checkPrecompressedVariants = _precompressedFormats.length > 0 && !endsWithSlash && reqRanges == null; - - try - { - // Directory? - if (Files.isDirectory(content.getPath())) - { - sendWelcome(content, pathInContext, endsWithSlash, request, response, callback); - return; - } - - // Strip slash? - if (endsWithSlash && pathInContext.length() > 1) - { - // TODO need helper code to edit URIs - String q = request.getHttpURI().getQuery(); - pathInContext = pathInContext.substring(0, pathInContext.length() - 1); - if (q != null && q.length() != 0) - pathInContext += "?" + q; - Response.sendRedirect(request, response, callback, URIUtil.addPaths(request.getContext().getContextPath(), pathInContext)); - return; - } - - // Conditional response? - if (passConditionalHeaders(request, response, content, callback)) - return; - - // Precompressed variant available? - Map precompressedContents = checkPrecompressedVariants ? content.getPrecompressedContents() : null; - if (precompressedContents != null && precompressedContents.size() > 0) - { - // Tell caches that response may vary by accept-encoding - response.getHeaders().add(HttpHeader.VARY.asString(), HttpHeader.ACCEPT_ENCODING.asString()); - - List preferredEncodings = getPreferredEncodingOrder(request); - CompressedContentFormat precompressedContentEncoding = getBestPrecompressedContent(preferredEncodings, precompressedContents.keySet()); - if (precompressedContentEncoding != null) - { - HttpContent precompressedContent = precompressedContents.get(precompressedContentEncoding); - if (LOG.isDebugEnabled()) - LOG.debug("precompressed={}", precompressedContent); - content = precompressedContent; - response.getHeaders().put(HttpHeader.CONTENT_ENCODING, precompressedContentEncoding.getEncoding()); - } - } - - // TODO this should be done by HttpContent#getContentEncoding - if (isGzippedContent(pathInContext)) - response.getHeaders().put(HttpHeader.CONTENT_ENCODING, "gzip"); - - // Send the data - sendData(request, response, callback, content, reqRanges); - } - // Can be thrown from contentFactory.getContent() call when using invalid characters - catch (InvalidPathException e) - { - if (LOG.isDebugEnabled()) - LOG.debug("InvalidPathException for pathInContext: {}", pathInContext, e); - Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404); - } - catch (IllegalArgumentException e) - { - LOG.warn("Failed to serve resource: {}", pathInContext, e); - if (!response.isCommitted()) - Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500); - } - } - - private List getPreferredEncodingOrder(Request request) - { - Enumeration headers = request.getHeaders().getValues(HttpHeader.ACCEPT_ENCODING.asString()); - if (!headers.hasMoreElements()) - return Collections.emptyList(); - - String key = headers.nextElement(); - if (headers.hasMoreElements()) - { - StringBuilder sb = new StringBuilder(key.length() * 2); - do - { - sb.append(',').append(headers.nextElement()); - } - while (headers.hasMoreElements()); - key = sb.toString(); - } - - List values = _preferredEncodingOrderCache.get(key); - if (values == null) - { - QuotedQualityCSV encodingQualityCSV = new QuotedQualityCSV(_preferredEncodingOrder); - encodingQualityCSV.addValue(key); - values = encodingQualityCSV.getValues(); - - // keep cache size in check even if we get strange/malicious input - if (_preferredEncodingOrderCache.size() > _encodingCacheSize) - _preferredEncodingOrderCache.clear(); - - _preferredEncodingOrderCache.put(key, values); - } - - return values; - } - - private boolean isGzippedContent(String path) - { - if (path == null || _gzipEquivalentFileExtensions == null) - return false; - - for (String suffix : _gzipEquivalentFileExtensions) - { - if (path.endsWith(suffix)) - return true; - } - return false; - } - - private CompressedContentFormat getBestPrecompressedContent(List preferredEncodings, java.util.Collection availableFormats) - { - if (availableFormats.isEmpty()) - return null; - - for (String encoding : preferredEncodings) - { - for (CompressedContentFormat format : availableFormats) - { - if (format.getEncoding().equals(encoding)) - return format; - } - - if ("*".equals(encoding)) - return availableFormats.iterator().next(); - - if (HttpHeaderValue.IDENTITY.asString().equals(encoding)) - return null; - } - return null; + return _resourceService.getContentFactory(); } /** - * @return true if the request was processed, false otherwise. + * @return Returns the resourceBase. */ - private boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException + public Resource getResourceBase() { - try - { - String ifm = null; - String ifnm = null; - String ifms = null; - long ifums = -1; - - // Find multiple fields by iteration as an optimization - for (HttpField field : request.getHeaders()) - { - if (field.getHeader() != null) - { - switch (field.getHeader()) - { - case IF_MATCH -> ifm = field.getValue(); - case IF_NONE_MATCH -> ifnm = field.getValue(); - case IF_MODIFIED_SINCE -> ifms = field.getValue(); - case IF_UNMODIFIED_SINCE -> ifums = DateParser.parseDate(field.getValue()); - default -> - { - } - } - } - } - - if (_etags) - { - String etag = content.getETagValue(); - if (ifm != null) - { - boolean match = false; - if (etag != null && !etag.startsWith("W/")) - { - QuotedCSV quoted = new QuotedCSV(true, ifm); - for (String etagWithSuffix : quoted) - { - if (CompressedContentFormat.tagEquals(etag, etagWithSuffix)) - { - match = true; - break; - } - } - } - - if (!match) - { - Response.writeError(request, response, callback, HttpStatus.PRECONDITION_FAILED_412); - return true; - } - } - - if (ifnm != null && etag != null) - { - // Handle special case of exact match OR gzip exact match - if (CompressedContentFormat.tagEquals(etag, ifnm) && ifnm.indexOf(',') < 0) - { - Response.writeError(request, response, callback, HttpStatus.NOT_MODIFIED_304); - return true; - } - - // Handle list of tags - QuotedCSV quoted = new QuotedCSV(true, ifnm); - for (String tag : quoted) - { - if (CompressedContentFormat.tagEquals(etag, tag)) - { - Response.writeError(request, response, callback, HttpStatus.NOT_MODIFIED_304); - return true; - } - } - - // If etag requires content to be served, then do not check if-modified-since - return false; - } - } - - // Handle if modified since - if (ifms != null) - { - //Get jetty's Response impl - String mdlm = content.getLastModifiedValue(); - if (ifms.equals(mdlm)) - { - Response.writeError(request, response, callback, HttpStatus.NOT_MODIFIED_304); - return true; - } - - long ifmsl = request.getHeaders().getDateField(HttpHeader.IF_MODIFIED_SINCE.asString()); - if (ifmsl != -1 && Files.getLastModifiedTime(content.getPath()).toMillis() / 1000 <= ifmsl / 1000) - { - Response.writeError(request, response, callback, HttpStatus.NOT_MODIFIED_304); - return true; - } - } - - // Parse the if[un]modified dates and compare to resource - if (ifums != -1 && Files.getLastModifiedTime(content.getPath()).toMillis() / 1000 > ifums / 1000) - { - Response.writeError(request, response, callback, HttpStatus.PRECONDITION_FAILED_412); - return true; - } - } - catch (IllegalArgumentException iae) - { - if (!response.isCommitted()) - Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400); - throw iae; - } - - return false; + return _resourceBase; } - protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, Request request, Response response, Callback callback) throws Exception + /** + * @return the cacheControl header to set on all static content. + */ + public String getCacheControl() { - // Redirect to directory - if (!endsWithSlash) - { - // TODO need helper code to edit URIs - StringBuilder buf = new StringBuilder(request.getHttpURI().asString()); - int param = buf.lastIndexOf(";"); - if (param < 0 || buf.lastIndexOf("/", param) > 0) - buf.append('/'); - else - buf.insert(param, '/'); - String q = request.getHttpURI().getQuery(); - if (q != null && q.length() != 0) - { - buf.append('?'); - buf.append(q); - } - response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, 0); - Response.sendRedirect(request, response, callback, buf.toString()); - return; - } - - // look for a welcome file - if (_welcomer.welcome(request, response, callback)) - return; - - if (!passConditionalHeaders(request, response, content, callback)) - sendDirectory(request, response, content, callback, pathInContext); + return _resourceService.getCacheControl(); } - private void sendDirectory(Request request, Response response, HttpContent httpContent, Callback callback, String pathInContext) throws IOException + /** + * @return file extensions that signify that a file is gzip compressed. Eg ".svgz" + */ + public List getGzipEquivalentFileExtensions() { - Path resource = httpContent.getPath(); - if (!_dirAllowed) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403); - return; - } - - String base = URIUtil.addEncodedPaths(request.getHttpURI().getPath(), URIUtil.SLASH); - String dir = getListHTML(resource, base, pathInContext.length() > 1, request.getHttpURI().getQuery()); - if (dir == null) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403); - return; - } - - byte[] data = dir.getBytes(StandardCharsets.UTF_8); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=utf-8"); - response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, data.length); - response.write(true, ByteBuffer.wrap(data), callback); + return _resourceService.getGzipEquivalentFileExtensions(); } - private String getListHTML(Path path, String base, boolean parent, String query) throws IOException + public MimeTypes getMimeTypes() { - // This method doesn't check aliases, so it is OK to canonicalize here. - base = URIUtil.canonicalPath(base); - if (base == null || !Files.isDirectory(path)) - return null; - - List items; - try (DirectoryStream directoryStream = Files.newDirectoryStream(path)) - { - Stream stream = StreamSupport.stream(directoryStream.spliterator(), false); - items = stream.collect(Collectors.toCollection(ArrayList::new)); - } - catch (IOException e) - { - LOG.debug("Directory list access failure", e); - return null; - } - - boolean sortOrderAscending = true; - String sortColumn = "N"; // name (or "M" for Last Modified, or "S" for Size) - - // check for query - if (query != null) - { - MultiMap params = new MultiMap<>(); - UrlEncoded.decodeUtf8To(query, 0, query.length(), params); - - String paramO = params.getString("O"); - String paramC = params.getString("C"); - if (StringUtil.isNotBlank(paramO)) - { - if (paramO.equals("A")) - { - sortOrderAscending = true; - } - else if (paramO.equals("D")) - { - sortOrderAscending = false; - } - } - if (StringUtil.isNotBlank(paramC)) - { - if (paramC.equals("N") || paramC.equals("M") || paramC.equals("S")) - { - sortColumn = paramC; - } - } - } - - // Perform sort - if (sortColumn.equals("M")) - items.sort(PathCollators.byLastModified(sortOrderAscending)); - else if (sortColumn.equals("S")) - items.sort(PathCollators.bySize(sortOrderAscending)); - else - items.sort(PathCollators.byName(sortOrderAscending)); - - String decodedBase = URIUtil.decodePath(base); - String title = "Directory: " + StringUtil.sanitizeXmlString(decodedBase); - - StringBuilder buf = new StringBuilder(4096); - - // Doctype Declaration (HTML5) - buf.append("\n"); - buf.append("\n"); - - // HTML Header - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append(""); - buf.append(title); - buf.append("\n"); - buf.append("\n"); - - // HTML Body - buf.append("\n"); - buf.append("

").append(title).append("

\n"); - - // HTML Table - final String ARROW_DOWN = "  ⇩"; - final String ARROW_UP = "  ⇧"; - - buf.append("\n"); - buf.append("\n"); - - String arrow = ""; - String order = "A"; - if (sortColumn.equals("N")) - { - if (sortOrderAscending) - { - order = "D"; - arrow = ARROW_UP; - } - else - { - order = "A"; - arrow = ARROW_DOWN; - } - } - - buf.append(""); - - arrow = ""; - order = "A"; - if (sortColumn.equals("M")) - { - if (sortOrderAscending) - { - order = "D"; - arrow = ARROW_UP; - } - else - { - order = "A"; - arrow = ARROW_DOWN; - } - } - - buf.append(""); - - arrow = ""; - order = "A"; - if (sortColumn.equals("S")) - { - if (sortOrderAscending) - { - order = "D"; - arrow = ARROW_UP; - } - else - { - order = "A"; - arrow = ARROW_DOWN; - } - } - buf.append("\n"); - buf.append("\n"); - - buf.append("\n"); - - String encodedBase = hrefEncodeURI(base); - - if (parent) - { - // Name - buf.append(""); - // Last Modified - buf.append(""); - // Size - buf.append(""); - buf.append("\n"); - } - - DateFormat dfmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); - for (Path item : items) - { - String name = item.getFileName().toString(); - if (StringUtil.isBlank(name)) - { - continue; // skip - } - - if (Files.isDirectory(item)) - { - name += URIUtil.SLASH; - } - - // Name - buf.append(""); - - // Last Modified - buf.append(""); - - // Size - buf.append("\n"); - } - buf.append("\n"); - buf.append("
"); - buf.append("Name").append(arrow); - buf.append(""); - buf.append("Last Modified").append(arrow); - buf.append(""); - buf.append("Size").append(arrow); - buf.append("
Parent Directory--
"); - buf.append(StringUtil.sanitizeXmlString(name)); - buf.append(" "); - buf.append(""); - long lastModified = Files.getLastModifiedTime(item).toMillis(); - if (lastModified > 0) - { - buf.append(dfmt.format(new Date(lastModified))); - } - buf.append(" "); - long length = Files.size(item); - if (length >= 0) - { - buf.append(String.format("%,d bytes", length)); - } - buf.append(" 
\n"); - buf.append("\n"); - - return buf.toString(); + return _mimeTypes; } - private static String hrefEncodeURI(String raw) + /** + * @return Returns the stylesheet as a Resource. + */ + public Resource getStylesheet() { - StringBuilder buf = null; - - loop: - for (int i = 0; i < raw.length(); i++) - { - char c = raw.charAt(i); - switch (c) - { - case '\'': - case '"': - case '<': - case '>': - buf = new StringBuilder(raw.length() << 1); - break loop; - default: - break; - } - } - if (buf == null) - return raw; - - for (int i = 0; i < raw.length(); i++) - { - char c = raw.charAt(i); - switch (c) - { - case '"' -> buf.append("%22"); - case '\'' -> buf.append("%27"); - case '<' -> buf.append("%3C"); - case '>' -> buf.append("%3E"); - default -> buf.append(c); - } - } - - return buf.toString(); + return _resourceService.getStylesheet(); } - private boolean sendData(Request request, Response response, Callback callback, HttpContent content, Enumeration reqRanges) throws IOException + public List getWelcomeFiles() { - long contentLength = content.getContentLengthValue(); - - if (LOG.isDebugEnabled()) - LOG.debug(String.format("sendData content=%s", content)); - - if (reqRanges == null || !reqRanges.hasMoreElements() || contentLength < 0) - { - // if there were no ranges, send entire entity - - // write the headers - putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH); - - // write the content - writeContent(response, callback, content); - } - else - { - throw new UnsupportedOperationException("TODO"); - // TODO rewrite with ByteChannel only which should simplify HttpContentRangeWriter as HttpContent's Path always provides a SeekableByteChannel - // but MultiPartOutputStream also needs to be rewritten. -/* - // Parse the satisfiable ranges - List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, contentLength); - - // if there are no satisfiable ranges, send 416 response - if (ranges == null || ranges.size() == 0) - { - putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH); - response.getHeaders().put(HttpHeader.CONTENT_RANGE, - InclusiveByteRange.to416HeaderRangeString(contentLength)); - sendStatus(416, response, callback); - return true; - } - - // if there is only a single valid range (must be satisfiable - // since were here now), send that range with a 216 response - if (ranges.size() == 1) - { - InclusiveByteRange singleSatisfiableRange = ranges.iterator().next(); - long singleLength = singleSatisfiableRange.getSize(); - putHeaders(response, content, singleLength); - response.setStatus(206); - if (!response.getHeaders().contains(HttpHeader.DATE.asString())) - response.getHeaders().addDateField(HttpHeader.DATE.asString(), System.currentTimeMillis()); - response.getHeaders().put(HttpHeader.CONTENT_RANGE, - singleSatisfiableRange.toHeaderRangeString(contentLength)); - writeContent(content, out, singleSatisfiableRange.getFirst(), singleLength); - return true; - } - - // multiple non-overlapping valid ranges cause a multipart - // 216 response which does not require an overall - // content-length header - // - putHeaders(response, content, NO_CONTENT_LENGTH); - String mimetype = content.getContentTypeValue(); - if (mimetype == null) - LOG.warn("Unknown mimetype for {}", request.getHttpURI()); - response.setStatus(206); - if (!response.getHeaders().contains(HttpHeader.DATE.asString())) - response.getHeaders().addDateField(HttpHeader.DATE.asString(), System.currentTimeMillis()); - - // If the request has a "Request-Range" header then we need to - // send an old style multipart/x-byteranges Content-Type. This - // keeps Netscape and acrobat happy. This is what Apache does. - String ctp; - if (request.getHeaders().get(HttpHeader.REQUEST_RANGE.asString()) != null) - ctp = "multipart/x-byteranges; boundary="; - else - ctp = "multipart/byteranges; boundary="; - MultiPartOutputStream multi = new MultiPartOutputStream(out); - response.setContentType(ctp + multi.getBoundary()); - - // calculate the content-length - int length = 0; - String[] header = new String[ranges.size()]; - int i = 0; - final int CRLF = "\r\n".length(); - final int DASHDASH = "--".length(); - final int BOUNDARY = multi.getBoundary().length(); - final int FIELD_SEP = ": ".length(); - for (InclusiveByteRange ibr : ranges) - { - header[i] = ibr.toHeaderRangeString(contentLength); - if (i > 0) // in-part - length += CRLF; - length += DASHDASH + BOUNDARY + CRLF; - if (mimetype != null) - length += HttpHeader.CONTENT_TYPE.asString().length() + FIELD_SEP + mimetype.length() + CRLF; - length += HttpHeader.CONTENT_RANGE.asString().length() + FIELD_SEP + header[i].length() + CRLF; - length += CRLF; - length += ibr.getSize(); - i++; - } - length += CRLF + DASHDASH + BOUNDARY + DASHDASH + CRLF; - response.setContentLength(length); - - try (RangeWriter rangeWriter = HttpContentRangeWriter.newRangeWriter(content)) - { - i = 0; - for (InclusiveByteRange ibr : ranges) - { - multi.startPart(mimetype, new String[]{HttpHeader.CONTENT_RANGE + ": " + header[i]}); - rangeWriter.writeTo(multi, ibr.getFirst(), ibr.getSize()); - i++; - } - } - - multi.close(); - */ - } - return true; - } - - private void writeContent(Response response, Callback callback, HttpContent content) throws IOException - { - ByteBuffer buffer = content.getBuffer(); - if (buffer != null) - response.write(true, buffer, callback); - else - new ContentWriterIteratingCallback(content, response, callback).iterate(); - } - - private void putHeaders(Response response, HttpContent content, long contentLength) - { - HttpFields.Mutable headers = response.getHeaders(); - - // TODO it is very inefficient to do many put's to a HttpFields, as each put is a full iteration. - // it might be better remove headers en masse and then just add the extras: -// headers.remove(EnumSet.of( -// HttpHeader.LAST_MODIFIED, -// HttpHeader.CONTENT_LENGTH, -// HttpHeader.CONTENT_TYPE, -// HttpHeader.CONTENT_ENCODING, -// HttpHeader.ETAG, -// HttpHeader.ACCEPT_RANGES, -// HttpHeader.CACHE_CONTROL -// )); -// HttpField lm = content.getLastModified(); -// if (lm != null) -// headers.add(lm); -// etc. - - HttpField lm = content.getLastModified(); - if (lm != null) - headers.put(lm); - - if (contentLength == USE_KNOWN_CONTENT_LENGTH) - { - headers.put(content.getContentLength()); - } - else if (contentLength > NO_CONTENT_LENGTH) - { - headers.putLongField(HttpHeader.CONTENT_LENGTH, contentLength); - } - - HttpField ct = content.getContentType(); - if (ct != null) - headers.put(ct); - - HttpField ce = content.getContentEncoding(); - if (ce != null) - headers.put(ce); - - if (_etags) - { - HttpField et = content.getETag(); - if (et != null) - headers.put(et); - } - - HttpFields.Mutable fields = response.getHeaders(); - if (_acceptRanges && !fields.contains(HttpHeader.ACCEPT_RANGES)) - fields.add(new PreEncodedHttpField(HttpHeader.ACCEPT_RANGES, "bytes")); - if (_cacheControl != null && !fields.contains(HttpHeader.CACHE_CONTROL)) - fields.add(_cacheControl); - } - - private boolean hasDefinedRange(Enumeration reqRanges) - { - return (reqRanges != null && reqRanges.hasMoreElements()); + return _welcomes; } /** @@ -988,7 +195,7 @@ public class ResourceHandler extends Handler.Wrapper */ public boolean isAcceptRanges() { - return _acceptRanges; + return _resourceService.isAcceptRanges(); } /** @@ -996,7 +203,7 @@ public class ResourceHandler extends Handler.Wrapper */ public boolean isDirAllowed() { - return _dirAllowed; + return _resourceService.isDirAllowed(); } /** @@ -1004,7 +211,7 @@ public class ResourceHandler extends Handler.Wrapper */ public boolean isEtags() { - return _etags; + return _resourceService.isEtags(); } /** @@ -1012,7 +219,7 @@ public class ResourceHandler extends Handler.Wrapper */ public CompressedContentFormat[] getPrecompressedFormats() { - return _precompressedFormats; + return _resourceService.getPrecompressedFormats(); } /** @@ -1020,7 +227,7 @@ public class ResourceHandler extends Handler.Wrapper */ public boolean isPathInfoOnly() { - return _pathInfoOnly; + return _resourceService.isPathInfoOnly(); } /** @@ -1028,7 +235,7 @@ public class ResourceHandler extends Handler.Wrapper */ public boolean isRedirectWelcome() { - return _redirectWelcome; + return _resourceService.isRedirectWelcome(); } /** @@ -1036,16 +243,17 @@ public class ResourceHandler extends Handler.Wrapper */ public void setAcceptRanges(boolean acceptRanges) { - _acceptRanges = acceptRanges; + _resourceService.setAcceptRanges(acceptRanges); } /** * @param base The resourceBase to server content from. If null the * context resource base is used. */ - public void setBaseResource(Path base) + public void setBaseResource(Resource base) { - _baseResource = base; + _resourceBase = base; + setupContentFactory(); } /** @@ -1053,7 +261,7 @@ public class ResourceHandler extends Handler.Wrapper */ public void setCacheControl(String cacheControl) { - _cacheControl = new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cacheControl); + _resourceService.setCacheControl(cacheControl); } /** @@ -1061,7 +269,7 @@ public class ResourceHandler extends Handler.Wrapper */ public void setDirAllowed(boolean dirAllowed) { - _dirAllowed = dirAllowed; + _resourceService.setDirAllowed(dirAllowed); } /** @@ -1069,7 +277,7 @@ public class ResourceHandler extends Handler.Wrapper */ public void setEtags(boolean etags) { - _etags = etags; + _resourceService.setEtags(etags); } /** @@ -1077,7 +285,7 @@ public class ResourceHandler extends Handler.Wrapper */ public void setGzipEquivalentFileExtensions(List gzipEquivalentFileExtensions) { - _gzipEquivalentFileExtensions = gzipEquivalentFileExtensions; + _resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions); } /** @@ -1086,25 +294,24 @@ public class ResourceHandler extends Handler.Wrapper */ public void setPrecompressedFormats(CompressedContentFormat[] precompressedFormats) { - _precompressedFormats = precompressedFormats; - _preferredEncodingOrder = stream(_precompressedFormats).map(CompressedContentFormat::getEncoding).toArray(String[]::new); + _resourceService.setPrecompressedFormats(precompressedFormats); + setupContentFactory(); } public void setEncodingCacheSize(int encodingCacheSize) { - _encodingCacheSize = encodingCacheSize; - if (encodingCacheSize > _preferredEncodingOrderCache.size()) - _preferredEncodingOrderCache.clear(); + _resourceService.setEncodingCacheSize(encodingCacheSize); } public int getEncodingCacheSize() { - return _encodingCacheSize; + return _resourceService.getEncodingCacheSize(); } public void setMimeTypes(MimeTypes mimeTypes) { _mimeTypes = mimeTypes; + setupContentFactory(); } /** @@ -1112,7 +319,7 @@ public class ResourceHandler extends Handler.Wrapper */ public void setPathInfoOnly(boolean pathInfoOnly) { - _pathInfoOnly = pathInfoOnly; + _resourceService.setPathInfoOnly(pathInfoOnly); } /** @@ -1122,29 +329,16 @@ public class ResourceHandler extends Handler.Wrapper */ public void setRedirectWelcome(boolean redirectWelcome) { - _redirectWelcome = redirectWelcome; + _resourceService.setRedirectWelcome(redirectWelcome); } /** * @param stylesheet The location of the stylesheet to be used as a String. */ - // TODO accept a Path instead of a String? + // TODO accept a Resource instead of a String? public void setStylesheet(String stylesheet) { - try - { - _stylesheet = Path.of(stylesheet); - if (!Files.exists(_stylesheet)) - { - LOG.warn("unable to find custom stylesheet: {}", stylesheet); - _stylesheet = null; - } - } - catch (Exception e) - { - LOG.warn("Invalid StyleSheet reference: {}", stylesheet, e); - throw new IllegalArgumentException(stylesheet); - } + _resourceService.setStylesheet(stylesheet); } public void setWelcomeFiles(List welcomeFiles) @@ -1152,239 +346,155 @@ public class ResourceHandler extends Handler.Wrapper _welcomes = welcomeFiles; } - private class PathContentFactory implements HttpContent.ContentFactory + private static class ResourceHandlerGenericRequest implements ResourceService.GenericRequest { - @Override - public HttpContent getContent(String path, int maxBuffer) throws IOException + private final Request request; + + ResourceHandlerGenericRequest(Request request) { - if (_precompressedFormats.length > 0) + this.request = request; + } + + @Override + public java.util.Collection getHeaders() + { + List fields = new ArrayList<>(); + HttpFields headers = request.getHeaders(); + Set names = headers.getFieldNamesCollection(); + for (String name : names) { - // Is the precompressed content cached? - Map compressedContents = new HashMap<>(); - for (CompressedContentFormat format : _precompressedFormats) + Enumeration values = headers.getValues(name); + while (values.hasMoreElements()) { - String compressedPathInContext = path + format.getExtension(); - - // Is there a precompressed resource? - PathHttpContent compressedContent = load(compressedPathInContext, null); - if (compressedContent != null) - compressedContents.put(format, compressedContent); + String value = values.nextElement(); + fields.add(new HttpField(name, value)); } - if (!compressedContents.isEmpty()) - return load(path, compressedContents); } - - return load(path, null); + return fields; } - private PathHttpContent load(String path, Map compressedEquivalents) + @Override + public Enumeration getHeaderValues(String name) { - if (path.startsWith("/")) - path = path.substring(1); - // TODO cache _baseResource.toUri() - Path resolved = Path.of(_baseResource.toUri().resolve(path)); - // TODO call alias checker - if (!Files.exists(resolved)) - return null; - String mimeType = _mimeTypes.getMimeByExtension(resolved.getFileName().toString()); - return new PathHttpContent(resolved, mimeType, compressedEquivalents); + return request.getHeaders().getValues(name); + } + + @Override + public long getHeaderDate(String name) + { + return request.getHeaders().getDateField(name); + } + + @Override + public HttpURI getHttpURI() + { + return request.getHttpURI(); + } + + @Override + public String getPathInContext() + { + return request.getPathInContext(); + } + + @Override + public String getContextPath() + { + return request.getContext().getContextPath(); } } - private static class PathHttpContent implements HttpContent + private static class ResourceHandlerGenericResponse implements ResourceService.GenericResponse { - private final Path _path; - private final PreEncodedHttpField _contentType; - private final String _characterEncoding; - private final MimeTypes.Type _mimeType; - private final Map _compressedContents; + private final Response response; - public PathHttpContent(Path path, String contentType, Map compressedEquivalents) + ResourceHandlerGenericResponse(Response response) { - _path = path; - _contentType = contentType == null ? null : new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, contentType); - _characterEncoding = _contentType == null ? null : MimeTypes.getCharsetFromContentType(contentType); - _mimeType = _contentType == null ? null : MimeTypes.CACHE.get(MimeTypes.getContentTypeWithoutCharset(contentType)); - _compressedContents = compressedEquivalents; + this.response = response; } @Override - public HttpField getContentType() + public boolean containsHeader(HttpHeader header) { - return _contentType; + return response.getHeaders().contains(header); } @Override - public String getContentTypeValue() + public void putHeader(HttpField header) { - return _contentType.getValue(); + response.getHeaders().put(header); } @Override - public String getCharacterEncoding() + public void putHeader(HttpHeader header, String value) { - return _characterEncoding; + response.getHeaders().put(header, value); } @Override - public MimeTypes.Type getMimeType() + public void putHeaderLong(HttpHeader name, long value) { - return _mimeType; + response.getHeaders().putLongField(name, value); } @Override - public HttpField getContentEncoding() + public boolean isCommitted() { - return null; + return response.isCommitted(); } @Override - public String getContentEncodingValue() + public int getOutputBufferSize() { - return null; + return response.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); } @Override - public HttpField getContentLength() + public boolean isUseOutputDirectByteBuffers() { - long cl = getContentLengthValue(); - return cl >= 0 ? new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, cl) : null; + return response.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); } @Override - public long getContentLengthValue() + public void sendRedirect(Callback callback, String uri) + { + Response.sendRedirect(response.getRequest(), response, callback, uri); + } + + @Override + public void writeError(Callback callback, int status) + { + Response.writeError(response.getRequest(), response, callback, status); + } + + @Override + public void write(HttpContent content, Callback callback) { try { - if (Files.isDirectory(_path)) - return NO_CONTENT_LENGTH; - return Files.size(_path); + ByteBuffer buffer = content.getBuffer(); + if (buffer != null) + writeLast(buffer, callback); + else + new ContentWriterIteratingCallback(content, response, callback).iterate(); } - catch (IOException e) + catch (Throwable x) { - return NO_CONTENT_LENGTH; + callback.failed(x); } } @Override - public HttpField getLastModified() - { - String lm = getLastModifiedValue(); - return lm != null ? new HttpField(HttpHeader.LAST_MODIFIED, lm) : null; - } - - @Override - public String getLastModifiedValue() - { - try - { - long lm = Files.getLastModifiedTime(_path).toMillis(); - return DateGenerator.formatDate(lm); - } - catch (IOException e) - { - return null; - } - } - - @Override - public HttpField getETag() - { - String weakETag = getWeakETag(); - return weakETag == null ? null : new HttpField(HttpHeader.ETAG, weakETag); - } - - @Override - public String getETagValue() - { - return getWeakETag(); - } - - private String getWeakETag() - { - StringBuilder b = new StringBuilder(32); - b.append("W/\""); - - String name = _path.toAbsolutePath().toString(); - int length = name.length(); - long lhash = 0; - for (int i = 0; i < length; i++) - { - lhash = 31 * lhash + name.charAt(i); - } - - Base64.Encoder encoder = Base64.getEncoder().withoutPadding(); - try - { - long lastModifiedTime = Files.getLastModifiedTime(_path).toMillis(); - b.append(encoder.encodeToString(longToBytes(lastModifiedTime ^ lhash))); - } - catch (IOException e) - { - LOG.debug("Unable to get last modified time of {}", _path, e); - return null; - } - try - { - long contentLengthValue = Files.size(_path); - b.append(encoder.encodeToString(longToBytes(contentLengthValue ^ lhash))); - } - catch (IOException e) - { - LOG.debug("Unable to get size of {}", _path, e); - return null; - } - b.append('"'); - return b.toString(); - } - - private static byte[] longToBytes(long value) - { - byte[] result = new byte[Long.BYTES]; - for (int i = Long.BYTES - 1; i >= 0; i--) - { - result[i] = (byte)(value & 0xFF); - value >>= 8; - } - return result; - } - - @Override - public Path getPath() - { - return _path; - } - - @Override - public Resource getResource() - { - // TODO cache or create in constructor? - return Resource.newResource(_path); - } - - @Override - public Map getPrecompressedContents() - { - return _compressedContents; - } - - @Override - public ByteBuffer getBuffer() - { - return null; - } - - @Override - public void release() + public void writeLast(ByteBuffer byteBuffer, Callback callback) { + response.write(true, byteBuffer, callback); } } - // TODO a ReadableByteChannel IteratingCallback that writes to a Response looks generic enough to be moved to some util module private static class ContentWriterIteratingCallback extends IteratingCallback { private final ReadableByteChannel source; - private final Content.Sink target; + private final Content.Sink sink; private final Callback callback; private final ByteBuffer byteBuffer; @@ -1395,12 +505,11 @@ public class ResourceHandler extends Handler.Wrapper // FileChannel fileChannel = (FileChannel) source; // fileChannel.transferTo(0, contentLength, c); - this.source = Files.newByteChannel(content.getPath()); - this.target = target; + this.source = Files.newByteChannel(content.getResource().getPath()); + this.sink = target; this.callback = callback; - HttpConfiguration httpConfiguration = target.getRequest().getConnectionMetaData().getHttpConfiguration(); - int outputBufferSize = httpConfiguration.getOutputBufferSize(); - boolean useOutputDirectByteBuffers = httpConfiguration.isUseOutputDirectByteBuffers(); + int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); + boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); this.byteBuffer = useOutputDirectByteBuffers ? ByteBuffer.allocateDirect(outputBufferSize) : ByteBuffer.allocate(outputBufferSize); // TODO use pool } @@ -1414,11 +523,11 @@ public class ResourceHandler extends Handler.Wrapper if (read == -1) { IO.close(source); - target.write(true, null, this); + sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; } byteBuffer.flip(); - target.write(false, byteBuffer, this); + sink.write(false, byteBuffer, this); return Action.SCHEDULED; } @@ -1434,78 +543,4 @@ public class ResourceHandler extends Handler.Wrapper callback.failed(x); } } - - public interface Welcomer - { - /** - * @return true if the request was processed, false otherwise. - */ - boolean welcome(Request request, Response response, Callback callback) throws Exception; - } - - private class DefaultWelcomer implements Welcomer - { - @Override - public boolean welcome(Request request, Response response, Callback callback) throws Exception - { - String pathInContext = request.getPathInContext(); - String welcome = getWelcomeFile(pathInContext); - if (welcome != null) - { - String contextPath = request.getContext().getContextPath(); - - if (_pathInfoOnly) - welcome = URIUtil.addPaths(contextPath, welcome); - - if (LOG.isDebugEnabled()) - LOG.debug("welcome={}", welcome); - - if (_redirectWelcome) - { - // Redirect to the index - response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, 0); - - // TODO need helper code to edit URIs - String uri = URIUtil.encodePath(URIUtil.addPaths(request.getContext().getContextPath(), welcome)); - String q = request.getHttpURI().getQuery(); - if (q != null && !q.isEmpty()) - uri += "?" + q; - - Response.sendRedirect(request, response, callback, uri); - return true; - } - - // Serve welcome file - HttpContent c = _contentFactory.getContent(welcome, request.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()); - sendData(request, response, callback, c, null); - return true; - } - return false; - } - - private String getWelcomeFile(String pathInContext) - { - if (_welcomes == null) - return null; - - for (String welcome : _welcomes) - { - // TODO this logic is similar to the one in PathContentFactory.getContent() - // TODO GW: This logic needs to be extensible so that a welcome file may be a servlet (yeah I know it shouldn't - // be called a welcome file then. So for example if /foo/index.jsp is the welcome file, we can't - // serve it's contents - rather we have to let the servlet layer to either a redirect or a RequestDispatcher to it. - // Worse yet, if there was a servlet mapped to /foo/index.html, then we need to be able to dispatch to it - // EVEN IF the file does not exist. - String welcomeInContext = URIUtil.addPaths(pathInContext, welcome); - String path = pathInContext; - if (path.startsWith("/")) - path = path.substring(1); - Path welcomePath = Path.of(_baseResource.toUri().resolve(path).resolve(welcome)); - if (Files.exists(welcomePath)) - return welcomeInContext; - } - // not found - return null; - } - } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java index 305e9187a26..6e1b4bc7aae 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java @@ -48,10 +48,12 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.resource.Resource; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.eclipse.jetty.http.HttpHeader.CONTENT_ENCODING; @@ -80,6 +82,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class ResourceHandlerTest { private static String LN = System.getProperty("line.separator"); + private static Path TEST_PATH; private Server _server; private HttpConfiguration _config; private ServerConnector _connector; @@ -90,7 +93,8 @@ public class ResourceHandlerTest @BeforeAll public static void setUpResources() throws Exception { - File dir = MavenTestingUtils.getTargetFile("test-classes/simple"); + TEST_PATH = MavenTestingUtils.getTargetFile("test-classes/simple").toPath(); + File dir = TEST_PATH.toFile(); File bigger = new File(dir, "bigger.txt"); File bigGz = new File(dir, "big.txt.gz"); File bigZip = new File(dir, "big.txt.zip"); @@ -163,11 +167,11 @@ public class ResourceHandlerTest _server.setConnectors(new Connector[]{_connector, _local}); _resourceHandler = new ResourceHandler(); - _resourceHandler.setBaseResource(MavenTestingUtils.getTargetFile("test-classes/simple").toPath()); _resourceHandler.setWelcomeFiles(List.of("welcome.txt")); _contextHandler = new ContextHandler("/resource"); _contextHandler.setHandler(_resourceHandler); + _contextHandler.setBaseResource(Resource.newResource(TEST_PATH)); _server.setHandler(_contextHandler); _server.start(); @@ -181,6 +185,7 @@ public class ResourceHandlerTest } @Test + @Disabled public void testPrecompressedGzipWorks() throws Exception { _resourceHandler.setPrecompressedFormats(new CompressedContentFormat[]{CompressedContentFormat.GZIP}); @@ -195,7 +200,7 @@ public class ResourceHandlerTest // Load big.txt.gz into a byte array and assert its contents byte per byte. try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Files.copy(_resourceHandler.getBaseResource().resolve("big.txt.gz"), baos); + Files.copy(TEST_PATH.resolve("big.txt.gz"), baos); assertThat(response1.getContentBytes(), is(baos.toByteArray())); } @@ -392,7 +397,7 @@ public class ResourceHandlerTest @Test public void testIfUnmodifiedSinceWithModifiedFile() throws Exception { - Path testFile = _resourceHandler.getBaseResource().resolve("test-unmodified-since-file.txt"); + Path testFile = TEST_PATH.resolve("test-unmodified-since-file.txt"); try (BufferedWriter bw = Files.newBufferedWriter(testFile)) { bw.write("some content\n"); @@ -632,7 +637,7 @@ public class ResourceHandlerTest public void testEtagIfNoneMatchModifiedFile() throws Exception { _resourceHandler.setEtags(true); - Path testFile = _resourceHandler.getBaseResource().resolve("test-etag-file.txt"); + Path testFile = TEST_PATH.resolve("test-etag-file.txt"); try (BufferedWriter bw = Files.newBufferedWriter(testFile)) { bw.write("some content\n"); @@ -668,7 +673,7 @@ public class ResourceHandlerTest public void testCachingFilesCached() throws Exception { // TODO explicitly turn on caching - long expectedSize = Files.size(_resourceHandler.getBaseResource().resolve("big.txt")); + long expectedSize = Files.size(TEST_PATH.resolve("big.txt")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); for (int i = 0; i < 10; i++) @@ -689,11 +694,12 @@ public class ResourceHandlerTest } @Test + @Disabled public void testCachingPrecompressedFilesCached() throws Exception { // TODO explicitly turn on caching - long expectedSize = Files.size(_resourceHandler.getBaseResource().resolve("big.txt")) + - Files.size(_resourceHandler.getBaseResource().resolve("big.txt.gz")); + long expectedSize = Files.size(TEST_PATH.resolve("big.txt")) + + Files.size(TEST_PATH.resolve("big.txt.gz")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); _resourceHandler.setPrecompressedFormats(new CompressedContentFormat[]{CompressedContentFormat.GZIP}); @@ -709,7 +715,7 @@ public class ResourceHandlerTest // Load big.txt.gz into a byte array and assert its contents byte per byte. try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Files.copy(_resourceHandler.getBaseResource().resolve("big.txt.gz"), baos); + Files.copy(TEST_PATH.resolve("big.txt.gz"), baos); assertThat(response1.getContentBytes(), is(baos.toByteArray())); } @@ -732,11 +738,12 @@ public class ResourceHandlerTest } @Test + @Disabled public void testCachingPrecompressedFilesCachedEtagged() throws Exception { // TODO explicitly turn on caching - long expectedSize = Files.size(_resourceHandler.getBaseResource().resolve("big.txt")) + - Files.size(_resourceHandler.getBaseResource().resolve("big.txt.gz")); + long expectedSize = Files.size(TEST_PATH.resolve("big.txt")) + + Files.size(TEST_PATH.resolve("big.txt.gz")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); _resourceHandler.setPrecompressedFormats(new CompressedContentFormat[]{CompressedContentFormat.GZIP}); @@ -757,7 +764,7 @@ public class ResourceHandlerTest // Load big.txt.gz into a byte array and assert its contents byte per byte. try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Files.copy(_resourceHandler.getBaseResource().resolve("big.txt.gz"), baos); + Files.copy(TEST_PATH.resolve("big.txt.gz"), baos); assertThat(response1.getContentBytes(), is(baos.toByteArray())); } @@ -801,7 +808,7 @@ public class ResourceHandlerTest public void testCachingWelcomeFileCached() throws Exception { // TODO explicitly turn on caching - long expectedSize = Files.size(_resourceHandler.getBaseResource().resolve("directory/welcome.txt")); + long expectedSize = Files.size(TEST_PATH.resolve("directory/welcome.txt")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); for (int i = 0; i < 10; i++) @@ -857,7 +864,7 @@ public class ResourceHandlerTest public void testCachingMaxCachedFileSizeRespected() throws Exception { // TODO explicitly turn on caching - long expectedSize = Files.size(_resourceHandler.getBaseResource().resolve("simple.txt")); + long expectedSize = Files.size(TEST_PATH.resolve("simple.txt")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); contentFactory.setMaxCachedFileSize((int)expectedSize); @@ -889,7 +896,7 @@ public class ResourceHandlerTest public void testCachingMaxCacheSizeRespected() throws Exception { // TODO explicitly turn on caching - long expectedSize = Files.size(_resourceHandler.getBaseResource().resolve("simple.txt")); + long expectedSize = Files.size(TEST_PATH.resolve("simple.txt")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); contentFactory.setMaxCacheSize((int)expectedSize); @@ -921,8 +928,8 @@ public class ResourceHandlerTest public void testCachingMaxCachedFilesRespected() throws Exception { // TODO explicitly turn on caching - long expectedSizeBig = Files.size(_resourceHandler.getBaseResource().resolve("big.txt")); - long expectedSizeSimple = Files.size(_resourceHandler.getBaseResource().resolve("simple.txt")); + long expectedSizeBig = Files.size(TEST_PATH.resolve("big.txt")); + long expectedSizeSimple = Files.size(TEST_PATH.resolve("simple.txt")); CachingContentFactory contentFactory = (CachingContentFactory)_resourceHandler.getContentFactory(); contentFactory.setMaxCachedFiles(1); @@ -954,7 +961,7 @@ public class ResourceHandlerTest public void testCachingRefreshing() throws Exception { // TODO explicitly turn on caching - Path tempPath = _resourceHandler.getBaseResource().resolve("temp.txt"); + Path tempPath = TEST_PATH.resolve("temp.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(tempPath)) { bufferedWriter.write("temp file"); diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java index 30df91ef449..0bcb0a39904 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java @@ -19,7 +19,6 @@ import java.net.URI; /** * ResourceFactory. */ -// TODO remove public interface ResourceFactory { /** diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/FileServer.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/FileServer.java index 25e8663cb56..fe56d8af0c2 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/FileServer.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/FileServer.java @@ -15,7 +15,7 @@ package org.eclipse.jetty.ee10.demos; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; +import java.util.List; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -43,8 +43,8 @@ public class FileServer // Configure the ResourceHandler. Setting the resource base indicates where the files should be served out of. // In this example it is the current directory but it can be configured to anything that the jvm has access to. resourceHandler.setDirAllowed(true); - resourceHandler.setWelcomeFiles(Arrays.asList(new String[]{"index.html"})); - resourceHandler.setBaseResource(baseResource.getPath()); + resourceHandler.setWelcomeFiles(List.of("index.html")); + resourceHandler.setBaseResource(baseResource); // Add the ResourceHandler to the server. server.setHandler(new Handler.Collection(resourceHandler, new DefaultHandler())); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/resources/fileserver.xml b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/resources/fileserver.xml index dd3a4dae9cb..35fa1127841 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/resources/fileserver.xml +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/resources/fileserver.xml @@ -25,10 +25,14 @@ index.html
- - - - + + + + + + + +
diff --git a/jetty-ee10/jetty-ee10-home/pom.xml b/jetty-ee10/jetty-ee10-home/pom.xml index 3c803bb4a13..8af1f281c39 100644 --- a/jetty-ee10/jetty-ee10-home/pom.xml +++ b/jetty-ee10/jetty-ee10-home/pom.xml @@ -125,6 +125,41 @@ ${source-assembly-directory}/lib + + copy-lib-transaction-api-deps + generate-resources + + copy + + + + + jakarta.transaction + jakarta.transaction-api + ${jakarta.transaction.api.version} + + + ${assembly-directory}/lib + + + + copy-lib-transaction-api-src-deps + generate-resources + + copy + + + + + jakarta.transaction + jakarta.transaction-api + ${jakarta.transaction.api.version} + sources + + + ${source-assembly-directory}/lib + + copy-ee10-annotations-deps generate-resources diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java index 7bd2449a090..2733ec1c9cd 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java @@ -13,8 +13,357 @@ package org.eclipse.jetty.ee10.servlet; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.CachingContentFactory; +import org.eclipse.jetty.http.CompressedContentFormat; +import org.eclipse.jetty.http.HttpContent; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.ResourceContentFactory; +import org.eclipse.jetty.server.ResourceService; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.URIUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class DefaultServlet extends HttpServlet { + private ResourceService _resourceService; + + @Override + public void init(ServletConfig config) throws ServletException + { + ContextHandler contextHandler = initContextHandler(config.getServletContext()); + + _resourceService = new ServletResourceService(); + MimeTypes mimeTypes = new MimeTypes(); + CompressedContentFormat[] precompressedFormats = new CompressedContentFormat[0]; + _resourceService.setContentFactory(new CachingContentFactory(new ResourceContentFactory(contextHandler.getResourceBase(), mimeTypes, precompressedFormats))); + + // TODO init other settings + } + + protected ContextHandler initContextHandler(ServletContext servletContext) + { + ContextHandler.Context context = ContextHandler.getCurrentContext(); + if (context == 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 context.getContextHandler(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + boolean useOutputDirectByteBuffers = true; + if (resp instanceof ServletContextResponse.ServletApiResponse servletApiResponse) + useOutputDirectByteBuffers = servletApiResponse.getResponse().getWrapped().getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); + + HttpContent content = _resourceService.getContent(req.getServletPath(), resp.getBufferSize()); + if (content == null) + { + // no content + resp.setStatus(404); + } + else + { + // serve content + try + { + _resourceService.doGet(new ServletGenericRequest(req), new ServletGenericResponse(resp, useOutputDirectByteBuffers), Callback.NOOP, content); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + } + + @Override + protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + doGet(req, resp); + } + + private static class ServletGenericRequest implements ResourceService.GenericRequest + { + private final HttpServletRequest request; + + ServletGenericRequest(HttpServletRequest request) + { + this.request = request; + } + + @Override + public Collection getHeaders() + { + List httpFields = new ArrayList<>(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) + { + String headerName = headerNames.nextElement(); + Enumeration headerValues = request.getHeaders(headerName); + while (headerValues.hasMoreElements()) + { + String headerValue = headerValues.nextElement(); + httpFields.add(new HttpField(headerName, headerValue)); + } + } + return httpFields; + } + + @Override + public Enumeration getHeaderValues(String name) + { + return request.getHeaders(name); + } + + @Override + public long getHeaderDate(String name) + { + return request.getDateHeader(name); + } + + @Override + public HttpURI getHttpURI() + { + return HttpURI.from(request.getRequestURI()); + } + + @Override + public String getPathInContext() + { + return request.getRequestURI(); + } + + @Override + public String getContextPath() + { + return request.getContextPath(); + } + } + + private static class ServletGenericResponse implements ResourceService.GenericResponse + { + private final HttpServletResponse response; + private final boolean useOutputDirectByteBuffers; + + public ServletGenericResponse(HttpServletResponse response, boolean useOutputDirectByteBuffers) + { + this.response = response; + this.useOutputDirectByteBuffers = useOutputDirectByteBuffers; + } + + @Override + public boolean containsHeader(HttpHeader header) + { + return response.containsHeader(header.asString()); + } + + @Override + public void putHeader(HttpField header) + { + response.addHeader(header.getName(), header.getValue()); + } + + @Override + public void putHeader(HttpHeader header, String value) + { + response.addHeader(header.asString(), value); + } + + @Override + public void putHeaderLong(HttpHeader header, long value) + { + response.addHeader(header.asString(), Long.toString(value)); + } + + @Override + public int getOutputBufferSize() + { + return response.getBufferSize(); + } + + @Override + public boolean isCommitted() + { + return response.isCommitted(); + } + + @Override + public boolean isUseOutputDirectByteBuffers() + { + return useOutputDirectByteBuffers; + } + + @Override + public void sendRedirect(Callback callback, String uri) + { + try + { + response.sendRedirect(uri); + callback.succeeded(); + } + catch (Throwable x) + { + callback.failed(x); + } + } + + @Override + public void writeError(Callback callback, int status) + { + response.setStatus(status); + callback.succeeded(); + } + + @Override + public void write(HttpContent content, Callback callback) + { + ByteBuffer buffer = content.getBuffer(); + if (buffer != null) + { + writeLast(buffer, callback); + } + else + { + try + { + try (InputStream inputStream = Files.newInputStream(content.getResource().getPath()); + OutputStream outputStream = response.getOutputStream()) + { + IO.copy(inputStream, outputStream); + } + callback.succeeded(); + } + catch (Throwable x) + { + callback.failed(x); + } + } + } + + @Override + public void writeLast(ByteBuffer byteBuffer, Callback callback) + { + try + { + ServletOutputStream outputStream = response.getOutputStream(); + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + outputStream.write(bytes); + outputStream.close(); + + callback.succeeded(); + } + catch (Throwable x) + { + callback.failed(x); + } + } + } + + private static class ServletResourceService extends ResourceService + { + private static final Logger LOG = LoggerFactory.getLogger(ServletResourceService.class); + + @Override + protected boolean welcome(GenericRequest rq, GenericResponse rs, Callback callback) throws IOException + { + HttpServletRequest request = ((ServletGenericRequest)rq).request; + HttpServletResponse response = ((ServletGenericResponse)rs).response; + String pathInContext = rq.getPathInContext(); + WelcomeFactory welcomeFactory = getWelcomeFactory(); + String welcome = welcomeFactory == null ? null : welcomeFactory.getWelcomeFile(pathInContext); + boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; + + if (welcome != null) + { + String servletPath = included ? (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) + : request.getServletPath(); + + if (isPathInfoOnly()) + welcome = URIUtil.addPaths(servletPath, welcome); + + if (LOG.isDebugEnabled()) + LOG.debug("welcome={}", welcome); + + ServletContext context = request.getServletContext(); + + if (isRedirectWelcome() || context == null) + { + // Redirect to the index + response.setContentLength(0); + + String uri = URIUtil.encodePath(URIUtil.addPaths(request.getContextPath(), welcome)); + String q = request.getQueryString(); + if (q != null && !q.isEmpty()) + uri += "?" + q; + + response.sendRedirect(response.encodeRedirectURL(uri)); + return true; + } + + RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome)); + if (dispatcher != null) + { + // Forward to the index + try + { + if (included) + { + dispatcher.include(request, response); + } + else + { + request.setAttribute("org.eclipse.jetty.server.welcome", welcome); + dispatcher.forward(request, response); + } + } + catch (ServletException e) + { + callback.failed(e); + } + } + return true; + } + return false; + } + + @Override + protected boolean passConditionalHeaders(GenericRequest request, GenericResponse response, HttpContent content, Callback callback) throws IOException + { + boolean included = ((ServletGenericRequest)request).request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; + if (included) + return true; + return super.passConditionalHeaders(request, response, content, callback); + } + } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java index bd8514926fb..dd5af02b679 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.ee10.servlet; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; @@ -55,6 +56,7 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -93,7 +95,8 @@ public class DispatcherTest _contextHandler.setContextPath("/context"); _contextCollection.addHandler(_contextHandler); _resourceHandler = new ResourceHandler(); - _resourceHandler.setBaseResource(MavenTestingUtils.getTestResourcePathDir("dispatchResourceTest").toAbsolutePath()); + Path basePath = MavenTestingUtils.getTestResourcePathDir("dispatchResourceTest"); + _resourceHandler.setBaseResource(Resource.newResource(basePath)); _resourceHandler.setPathInfoOnly(true); ContextHandler resourceContextHandler = new ContextHandler("/resource"); resourceContextHandler.setHandler(_resourceHandler); diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/CachedContentFactory.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/CachedContentFactory.java index 5be7f7c6842..74d1e90f12e 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/CachedContentFactory.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/CachedContentFactory.java @@ -16,7 +16,6 @@ package org.eclipse.jetty.ee9.nested; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; -import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -38,6 +37,7 @@ import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.PrecompressedHttpContent; import org.eclipse.jetty.http.ResourceHttpContent; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.slf4j.Logger; @@ -438,12 +438,6 @@ public class CachedContentFactory implements HttpContent.ContentFactory return _key != null; } - @Override - public Path getPath() - { - return _resource.getPath(); - } - @Override public Resource getResource() { diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java index 556a4b7ab98..8ff61ecae56 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java @@ -1314,7 +1314,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable try { - ReadableByteChannel rbc = Files.newByteChannel(httpContent.getPath()); + ReadableByteChannel rbc = Files.newByteChannel(httpContent.getResource().getPath()); sendContent(rbc, callback); } catch (Throwable x) diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResourceService.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResourceService.java index 3bd01c6d827..1771e40661e 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResourceService.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResourceService.java @@ -859,7 +859,7 @@ public class ResourceService } // Use a ranged writer - try (SeekableByteChannelRangeWriter rangeWriter = new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(content.getPath()))) + try (SeekableByteChannelRangeWriter rangeWriter = new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(content.getResource().getPath()))) { rangeWriter.writeTo(out, start, contentLength); } diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/resource/HttpContentRangeWriter.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/resource/HttpContentRangeWriter.java index e27aaf7cd52..038330d89db 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/resource/HttpContentRangeWriter.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/resource/HttpContentRangeWriter.java @@ -44,6 +44,6 @@ public class HttpContentRangeWriter if (buffer != null) return new ByteBufferRangeWriter(buffer); - return new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(content.getPath())); + return new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(content.getResource().getPath())); } } diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/GzipHandlerIsHandledTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/GzipHandlerIsHandledTest.java index 66b1bb4fa11..4c9bb6791af 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/GzipHandlerIsHandledTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/GzipHandlerIsHandledTest.java @@ -26,8 +26,8 @@ import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -37,8 +37,6 @@ import static org.hamcrest.Matchers.is; /** * Tests of behavior of GzipHandler when Request.isHandled() or Response.isCommitted() is true */ -// TODO: re-enable when the PathResource work has been integrated. -@Disabled() public class GzipHandlerIsHandledTest { public WorkDir workDir; @@ -74,9 +72,8 @@ public class GzipHandlerIsHandledTest Handler.Collection handlers = new Handler.Collection(); ResourceHandler resourceHandler = new ResourceHandler(); - resourceHandler.setBaseResource(workDir.getPath()); - // TODO: fix when the PathResource work has been integrated. -// resourceHandler.setDirectoriesListed(true); + resourceHandler.setBaseResource(Resource.newResource(workDir.getPath())); + resourceHandler.setDirAllowed(true); resourceHandler.setHandler(new EventHandler(events, "ResourceHandler")); GzipHandler gzipHandler = new GzipHandler(); @@ -91,14 +88,9 @@ public class GzipHandlerIsHandledTest assertThat("response.status", response.getStatus(), is(200)); // we should have received a directory listing from the ResourceHandler assertThat("response.content", response.getContentAsString(), containsString("Directory: /")); - // resource handler should have handled the request - // the gzip handler and default handlers should have been executed, seeing as this is a HandlerCollection - // but the gzip handler should not have acted on the request, as the response is committed - assertThat("One event should have been recorded", events.size(), is(1)); - // the event handler should see the request.isHandled = true - // and response.isCommitted = true as the gzip handler didn't really do anything due to these - // states and let the wrapped handler (the EventHandler in this case) make the call on what it should do. - assertThat("Event indicating that GzipHandler-wrapped-handler ran", events.remove(), is("GzipHandler-wrapped-handler")); + // resource handler should have handled the request; + // hence the gzip handler and default handlers should not have been executed + assertThat("Zero event should have been recorded", events.size(), is(0)); } private static class EventHandler extends Handler.Abstract