org.eclipse.jetty.ee8.nested.ResourceService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-ee8-nested Show documentation
Show all versions of jetty-ee8-nested Show documentation
The jetty core handler adapted to legacy ee8 handler.
The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee8.nested;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import javax.servlet.AsyncContext;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee8.nested.resource.HttpContentRangeWriter;
import org.eclipse.jetty.ee8.nested.resource.RangeWriter;
import org.eclipse.jetty.ee8.nested.resource.SeekableByteChannelRangeWriter;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.EtagUtils;
import org.eclipse.jetty.http.HttpDateTime;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.http.QuotedQualityCSV;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.http.content.PreCompressedHttpContent;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.ResourceListing;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
/**
* Abstract resource service, used by DefaultServlet and ResourceHandler
*/
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 HttpContent.Factory _contentFactory;
private WelcomeFactory _welcomeFactory;
private boolean _acceptRanges = true;
private boolean _dirAllowed = true;
private boolean _redirectWelcome = false;
private CompressedContentFormat[] _precompressedFormats = CompressedContentFormat.NONE;
private String[] _preferredEncodingOrder = new String[0];
private final Map> _preferredEncodingOrderCache = new ConcurrentHashMap<>();
private int _encodingCacheSize = 100;
private boolean _pathInfoOnly = false;
private boolean _etags = false;
private HttpField _cacheControl;
private List _gzipEquivalentFileExtensions;
public HttpContent.Factory getHttpContentFactory() {
return _contentFactory;
}
public void setHttpContentFactory(HttpContent.Factory contentFactory) {
_contentFactory = contentFactory;
}
public WelcomeFactory getWelcomeFactory() {
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(CompressedContentFormat::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;
} else {
if (cacheControl.getHeader() != HttpHeader.CACHE_CONTROL)
throw new IllegalArgumentException("!Cache-Control");
_cacheControl = cacheControl instanceof PreEncodedHttpField ? cacheControl : new PreEncodedHttpField(cacheControl.getHeader(), cacheControl.getValue());
}
}
public List getGzipEquivalentFileExtensions() {
return _gzipEquivalentFileExtensions;
}
public void setGzipEquivalentFileExtensions(List gzipEquivalentFileExtensions) {
_gzipEquivalentFileExtensions = gzipEquivalentFileExtensions;
}
public boolean doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String servletPath;
String pathInfo;
Enumeration reqRanges = null;
boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
if (included) {
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();
}
} 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 (!_acceptRanges && reqRanges != null) {
reqRanges = null;
response.setHeader(HttpHeader.ACCEPT_RANGES.asString(), "none");
}
}
String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
boolean endsWithSlash = (pathInfo == null ? (_pathInfoOnly ? "" : servletPath) : pathInfo).endsWith("/");
HttpContent content = null;
boolean releaseContent = true;
try {
// Find the content
content = _contentFactory.getContent(pathInContext);
if (LOG.isDebugEnabled())
LOG.debug("content={}", content);
// Not found?
if (content == null || Resources.missing(content.getResource())) {
if (included)
throw new FileNotFoundException("!" + pathInContext);
notFound(request, response);
return response.isCommitted();
}
ContextHandler contextHandler = ContextHandler.getContextHandler(request.getServletContext());
if (contextHandler != null && !contextHandler.checkAlias(pathInContext, content.getResource())) {
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;
}
// Strip slash?
if (!included && endsWithSlash && pathInContext.length() > 1) {
String q = request.getQueryString();
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;
}
// Conditional response?
if (!included && !passConditionalHeaders(request, response, content))
return true;
// Get pre-compressed content.
if (_precompressedFormats.length > 0) {
List preferredEncodingOrder = getPreferredEncodingOrder(request);
if (!preferredEncodingOrder.isEmpty()) {
for (String encoding : preferredEncodingOrder) {
CompressedContentFormat contentFormat = isEncodingAvailable(encoding, Arrays.asList(_precompressedFormats));
if (contentFormat == null)
continue;
HttpContent preCompressedContent = _contentFactory.getContent(pathInContext + contentFormat.getExtension());
if (preCompressedContent == null)
continue;
content = new PreCompressedHttpContent(content, preCompressedContent, contentFormat);
break;
}
}
}
// Set content encoding related headers.
if (_precompressedFormats.length > 0)
response.setHeader(HttpHeader.VARY.asString(), HttpHeader.ACCEPT_ENCODING.asString());
HttpField contentEncoding = content.getContentEncoding();
if (contentEncoding != null)
response.setHeader(contentEncoding.getName(), contentEncoding.getValue());
else if (isGzippedContent(pathInContext))
response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), "gzip");
// Send the data
releaseContent = sendData(request, response, included, 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();
} catch (IllegalArgumentException e) {
LOG.warn("Failed to serve resource: {}", pathInContext, e);
if (!response.isCommitted())
response.sendError(500, e.getMessage());
} finally {
if (releaseContent) {
if (content != null)
content.release();
}
}
return true;
}
private List getPreferredEncodingOrder(HttpServletRequest request) {
Enumeration headers = request.getHeaders(HttpHeader.ACCEPT_ENCODING.asString());
if (!headers.hasMoreElements())
return emptyList();
String key = headers.nextElement();
if (headers.hasMoreElements()) {
StringBuilder sb = new StringBuilder(key.length() * 2);
sb.append(key);
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 CompressedContentFormat isEncodingAvailable(String encoding, Collection availableFormats) {
if (availableFormats.isEmpty())
return null;
for (CompressedContentFormat format : availableFormats) {
if (format.getEncoding().equals(encoding))
return format;
}
if ("*".equals(encoding))
return availableFormats.iterator().next();
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.setHeader(HttpHeader.ETAG.asString(), etag.get());
response.flushBuffer();
}
/* Check modification date headers.
*/
protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, HttpContent content) throws IOException {
try {
String ifm = null;
String ifnm = null;
String ifms = null;
String ifums = null;
if (request instanceof Request) {
// Find multiple fields by iteration as an optimization
for (HttpField field : ((Request) request).getHttpFields()) {
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 = field.getValue();
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.getHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
}
if (_etags) {
String etag = content.getETagValue();
if (ifm != null) {
boolean match = false;
if (etag != null) {
QuotedCSV quoted = new QuotedCSV(true, ifm);
for (String etagWithSuffix : quoted) {
if (EtagUtils.matches(etag, etagWithSuffix)) {
match = true;
break;
}
}
}
if (!match) {
sendStatus(response, HttpServletResponse.SC_PRECONDITION_FAILED, null);
return false;
}
}
if (ifnm != null && etag != null) {
// Handle special case of exact match OR gzip exact match
if (EtagUtils.matches(etag, ifnm) && ifnm.indexOf(',') < 0) {
sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, ifnm::toString);
return false;
}
// Handle list of tags
QuotedCSV quoted = new QuotedCSV(true, ifnm);
for (String tag : quoted) {
if (EtagUtils.matches(etag, tag)) {
sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, tag::toString);
return false;
}
}
// If etag requires content to be served, then do not check if-modified-since
return true;
}
}
// Handle if modified since
if (ifms != null && ifnm == null) {
//Get jetty's Response impl
String mdlm = content.getLastModifiedValue();
if (ifms.equals(mdlm)) {
sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, content::getETagValue);
return false;
}
// TODO: what should we do when we get a crappy date?
long ifmsl = HttpDateTime.parseToEpoch(ifms);
if (ifmsl != -1) {
long lm = content.getResource().lastModified().toEpochMilli();
if (lm != -1 && lm / 1000 <= ifmsl / 1000) {
sendStatus(response, HttpServletResponse.SC_NOT_MODIFIED, content::getETagValue);
return false;
}
}
}
// Parse the if[un]modified dates and compare to resource
if (ifums != null && ifm == null) {
// TODO: what should we do when we get a crappy date?
long ifumsl = HttpDateTime.parseToEpoch(ifums);
if (ifumsl != -1) {
long lm = content.getResource().lastModified().toEpochMilli();
if (lm != -1 && lm / 1000 > ifumsl / 1000) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
}
} catch (IllegalArgumentException iae) {
if (!response.isCommitted())
response.sendError(400, iae.getMessage());
throw iae;
}
return true;
}
protected void sendDirectory(HttpServletRequest request, HttpServletResponse response, Resource resource, String pathInContext) throws IOException {
if (!_dirAllowed) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
byte[] data;
String base = URIUtil.addEncodedPaths(request.getRequestURI(), "/");
String dir = ResourceListing.getAsXHTML(resource, base, pathInContext.length() > 1, request.getQueryString());
if (dir == null) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "No directory");
return;
}
data = dir.getBytes(StandardCharsets.UTF_8);
response.setContentType("text/html;charset=utf-8");
response.setContentLength(data.length);
response.getOutputStream().write(data);
}
protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, final 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();
} catch (IllegalStateException e) {
out = new WriterOutputStream(response.getWriter());
// there may be data in writer buffer, so assume written
written = true;
}
if (LOG.isDebugEnabled())
LOG.debug(String.format("sendData content=%s out=%s async=%b", content, out, request.isAsyncSupported()));
if (reqRanges == null || !reqRanges.hasMoreElements() || content_length < 0) {
// if there were no ranges, send entire entity
if (include) {
// write without headers
writeContent(content, out, 0, content_length);
} else // else if we can't do a bypass write because of wrapping
if (written) {
// write normally
putHeaders(response, content, Response.NO_CONTENT_LENGTH);
writeContent(content, out, 0, content_length);
} else // else do a bypass write
{
// write the headers
putHeaders(response, content, Response.USE_KNOWN_CONTENT_LENGTH);
// write the content asynchronously if supported
if (request.isAsyncSupported()) {
final AsyncContext context = request.startAsync();
context.setTimeout(0);
((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);
}
} else {
// Parse the satisfiable ranges
List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);
// if there are no satisfiable ranges, send 416 response
if (ranges == null || ranges.size() == 0) {
response.setContentLength(0);
response.setHeader(HttpHeader.CONTENT_RANGE.asString(), InclusiveByteRange.to416HeaderRangeString(content_length));
sendStatus(response, HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, null);
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(HttpServletResponse.SC_PARTIAL_CONTENT);
if (!response.containsHeader(HttpHeader.DATE.asString()))
response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis());
response.setHeader(HttpHeader.CONTENT_RANGE.asString(), singleSatisfiableRange.toHeaderRangeString(content_length));
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, Response.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());
// 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)
ctp = "multipart/x-byteranges; boundary=";
else
ctp = "multipart/byteranges; boundary=";
MultiPartOutputStream multi = new MultiPartOutputStream(out);
response.setContentType(ctp + multi.getBoundary());
// calculate the content-length
long 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(content_length);
if (// in-part
i > 0)
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.setContentLengthLong(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 static void writeContent(HttpContent content, OutputStream out, long start, long contentLength) throws IOException {
// attempt efficient ByteBuffer based write
ByteBuffer buffer = content.getByteBuffer();
if (buffer != null) {
// no need to modify buffer pointers when whole content is requested
if (start != 0 || content.getResource().length() != contentLength) {
buffer = buffer.asReadOnlyBuffer();
buffer.position((int) (buffer.position() + start));
buffer.limit((int) (buffer.position() + contentLength));
}
BufferUtil.writeTo(buffer, out);
return;
}
// Use a ranged writer if resource backed by path
Path path = content.getResource().getPath();
if (path != null) {
try (SeekableByteChannelRangeWriter rangeWriter = new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(path))) {
rangeWriter.writeTo(out, start, contentLength);
}
return;
}
// Perform ranged write
try (InputStream input = IOResources.asInputStream(content.getResource())) {
input.skipNBytes(start);
IO.copy(input, out, contentLength);
}
}
protected void putHeaders(HttpServletResponse response, HttpContent content, long contentLength) {
if (response instanceof Response r) {
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.setHeader(ACCEPT_RANGES.getName(), ACCEPT_RANGES.getValue());
if (_cacheControl != null && !response.containsHeader(HttpHeader.CACHE_CONTROL.asString()))
response.setHeader(_cacheControl.getName(), _cacheControl.getValue());
}
}
public interface WelcomeFactory {
/**
* Finds a matching welcome file for the supplied {@link Resource}.
*
* @param pathInContext the path of the request
* @return The path of the matching welcome file in context or null.
*/
String getWelcomeFile(String pathInContext) throws IOException;
}
}