Implement HttpContent.writeTo() async API (#12020)

#8790 implement HttpContent.writeTo() async API

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Ludovic Orban 2024-07-24 14:07:31 +02:00 committed by GitHub
parent d43da5279c
commit 834db77605
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 664 additions and 920 deletions

View File

@ -30,28 +30,52 @@ public class InputStreamRequestContent extends InputStreamContentSource implemen
{
private final String contentType;
/**
* @deprecated use {@link #InputStreamRequestContent(String, InputStream, ByteBufferPool.Sized)} instead.
*/
@Deprecated
public InputStreamRequestContent(InputStream stream)
{
this(stream, 4096);
}
/**
* @deprecated use {@link #InputStreamRequestContent(String, InputStream, ByteBufferPool.Sized)} instead.
*/
@Deprecated
public InputStreamRequestContent(InputStream stream, int bufferSize)
{
this("application/octet-stream", stream, bufferSize);
}
/**
* @deprecated use {@link #InputStreamRequestContent(String, InputStream, ByteBufferPool.Sized)} instead.
*/
@Deprecated
public InputStreamRequestContent(String contentType, InputStream stream, int bufferSize)
{
this(contentType, stream);
setBufferSize(bufferSize);
this(contentType, stream, new ByteBufferPool.Sized(null, false, bufferSize));
}
/**
* @deprecated use {@link #InputStreamRequestContent(String, InputStream, ByteBufferPool.Sized)} instead.
*/
@Deprecated
public InputStreamRequestContent(String contentType, InputStream stream)
{
this(contentType, stream, null);
}
/**
* @deprecated use {@link #InputStreamRequestContent(String, InputStream, ByteBufferPool.Sized)} instead.
*/
@Deprecated
public InputStreamRequestContent(String contentType, InputStream stream, ByteBufferPool bufferPool)
{
this(contentType, stream, new ByteBufferPool.Sized(bufferPool));
}
public InputStreamRequestContent(String contentType, InputStream stream, ByteBufferPool.Sized bufferPool)
{
super(stream, bufferPool);
this.contentType = contentType;

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.http.content;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Set;
import java.util.SortedSet;
@ -30,9 +29,12 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.io.Retainable;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.Resource;
@ -328,30 +330,19 @@ public class CachingHttpContentFactory implements HttpContent.Factory
boolean isValid = true;
// Read the content into memory if the HttpContent does not already have a buffer.
RetainableByteBuffer buffer;
ByteBuffer byteBuffer = httpContent.getByteBuffer();
if (byteBuffer == null)
RetainableByteBuffer buffer = null;
try
{
try
{
if (_contentLengthValue <= _maxCachedFileSize)
buffer = IOResources.toRetainableByteBuffer(httpContent.getResource(), _bufferPool, _useDirectByteBuffers);
else
buffer = null;
}
catch (Throwable t)
{
buffer = null;
isValid = false;
if (LOG.isDebugEnabled())
LOG.warn("Failed to read Resource: {}", httpContent.getResource(), t);
else
LOG.warn("Failed to read Resource: {} - {}", httpContent.getResource(), t.toString());
}
if (_contentLengthValue <= _maxCachedFileSize)
buffer = IOResources.toRetainableByteBuffer(httpContent.getResource(), _bufferPool, _useDirectByteBuffers);
}
else
catch (Throwable t)
{
buffer = RetainableByteBuffer.wrap(byteBuffer);
isValid = false;
if (LOG.isDebugEnabled())
LOG.warn("Failed to read Resource: {}", httpContent.getResource(), t);
else
LOG.warn("Failed to read Resource: {} - {}", httpContent.getResource(), t.toString());
}
_buffer = buffer;
@ -373,12 +364,6 @@ public class CachingHttpContentFactory implements HttpContent.Factory
return _contentLengthValue;
}
@Override
public ByteBuffer getByteBuffer()
{
return _buffer == null ? null : _buffer.getByteBuffer().asReadOnlyBuffer();
}
@Override
public long getBytesOccupied()
{
@ -403,6 +388,20 @@ public class CachingHttpContentFactory implements HttpContent.Factory
return _cacheKey;
}
@Override
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
try
{
sink.write(true, BufferUtil.slice(_buffer.getByteBuffer(), (int)offset, (int)length), callback);
}
catch (Throwable x)
{
// BufferUtil.slice() may fail if offset and/or length are out of bounds.
callback.failed(x);
}
}
@Override
public boolean retain()
{
@ -596,9 +595,9 @@ public class CachingHttpContentFactory implements HttpContent.Factory
}
@Override
public ByteBuffer getByteBuffer()
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
return null;
sink.write(true, BufferUtil.EMPTY_BUFFER, callback);
}
@Override

View File

@ -15,10 +15,13 @@ package org.eclipse.jetty.http.content;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.Objects;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IteratingNestedCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -26,9 +29,11 @@ public class FileMappingHttpContentFactory implements HttpContent.Factory
{
private static final Logger LOG = LoggerFactory.getLogger(FileMappingHttpContentFactory.class);
private static final int DEFAULT_MIN_FILE_SIZE = 16 * 1024;
private static final int DEFAULT_MAX_BUFFER_SIZE = Integer.MAX_VALUE;
private final HttpContent.Factory _factory;
private final int _minFileSize;
private final int _maxBufferSize;
/**
* Construct a {@link FileMappingHttpContentFactory} which can use file mapped buffers.
@ -39,7 +44,7 @@ public class FileMappingHttpContentFactory implements HttpContent.Factory
*/
public FileMappingHttpContentFactory(HttpContent.Factory factory)
{
this(factory, DEFAULT_MIN_FILE_SIZE);
this(factory, DEFAULT_MIN_FILE_SIZE, DEFAULT_MAX_BUFFER_SIZE);
}
/**
@ -47,11 +52,13 @@ public class FileMappingHttpContentFactory implements HttpContent.Factory
*
* @param factory the wrapped {@link HttpContent.Factory} to use.
* @param minFileSize the minimum size of an {@link HttpContent} before trying to use a file mapped buffer.
* @param maxBufferSize the maximum size of the memory mapped buffers
*/
public FileMappingHttpContentFactory(HttpContent.Factory factory, int minFileSize)
public FileMappingHttpContentFactory(HttpContent.Factory factory, int minFileSize, int maxBufferSize)
{
_factory = Objects.requireNonNull(factory);
_minFileSize = minFileSize;
_maxBufferSize = maxBufferSize;
}
@Override
@ -60,71 +67,147 @@ public class FileMappingHttpContentFactory implements HttpContent.Factory
HttpContent content = _factory.getContent(path);
if (content != null)
{
long contentLength = content.getContentLengthValue();
if (contentLength > _minFileSize && contentLength < Integer.MAX_VALUE)
return new FileMappedHttpContent(content);
try
{
long contentLength = content.getContentLengthValue();
if (contentLength < _minFileSize)
return content;
return contentLength <= _maxBufferSize ? new SingleBufferFileMappedHttpContent(content) : new MultiBufferFileMappedHttpContent(content, _maxBufferSize);
}
catch (IOException e)
{
if (LOG.isDebugEnabled())
LOG.debug("Error getting Mapped Buffer", e);
// Fall through to return the content gotten from the factory.
}
}
return content;
}
private static class FileMappedHttpContent extends HttpContent.Wrapper
private static class SingleBufferFileMappedHttpContent extends HttpContent.Wrapper
{
private static final ByteBuffer SENTINEL_BUFFER = BufferUtil.allocate(0);
private final ByteBuffer _buffer;
private final AutoLock _lock = new AutoLock();
private final HttpContent _content;
private volatile ByteBuffer _buffer;
public FileMappedHttpContent(HttpContent content)
private SingleBufferFileMappedHttpContent(HttpContent content) throws IOException
{
super(content);
this._content = content;
Path path = content.getResource().getPath();
if (path == null)
throw new IOException("Cannot memory map Content whose Resource is not backed by a Path: " + content.getResource());
_buffer = BufferUtil.toMappedBuffer(path);
}
@Override
public ByteBuffer getByteBuffer()
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
ByteBuffer buffer = _buffer;
if (buffer != null)
return (buffer == SENTINEL_BUFFER) ? super.getByteBuffer() : buffer.asReadOnlyBuffer();
try (AutoLock lock = _lock.lock())
try
{
if (_buffer == null)
_buffer = getMappedByteBuffer();
return (_buffer == SENTINEL_BUFFER) ? super.getByteBuffer() : _buffer.asReadOnlyBuffer();
sink.write(true, BufferUtil.slice(_buffer, (int)offset, (int)length), callback);
}
catch (Throwable x)
{
callback.failed(x);
}
}
@Override
public long getBytesOccupied()
{
ByteBuffer buffer = _buffer;
if (buffer != null)
return (buffer == SENTINEL_BUFFER) ? super.getBytesOccupied() : 0;
return _buffer.remaining();
}
}
try (AutoLock lock = _lock.lock())
private static class MultiBufferFileMappedHttpContent extends HttpContent.Wrapper
{
private final ByteBuffer[] _buffers;
private final int maxBufferSize;
private final long _bytesOccupied;
private MultiBufferFileMappedHttpContent(HttpContent content, int maxBufferSize) throws IOException
{
super(content);
this.maxBufferSize = maxBufferSize;
Path path = content.getResource().getPath();
if (path == null)
throw new IOException("Cannot memory map Content whose Resource is not backed by a Path: " + content.getResource());
long contentLength = content.getContentLengthValue();
int bufferCount = Math.toIntExact(contentLength / maxBufferSize);
_buffers = new ByteBuffer[bufferCount];
long currentPos = 0L;
long total = 0L;
for (int i = 0; i < _buffers.length; i++)
{
if (_buffer == null)
_buffer = getMappedByteBuffer();
return (_buffer == SENTINEL_BUFFER) ? super.getBytesOccupied() : 0;
long len = Math.min(contentLength - currentPos, maxBufferSize);
_buffers[i] = BufferUtil.toMappedBuffer(path, currentPos, len);
currentPos += len;
total += _buffers[i].remaining();
}
_bytesOccupied = total;
}
private ByteBuffer getMappedByteBuffer()
@Override
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
try
{
ByteBuffer byteBuffer = BufferUtil.toMappedBuffer(_content.getResource().getPath());
return (byteBuffer == null) ? SENTINEL_BUFFER : byteBuffer;
}
catch (Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("Error getting Mapped Buffer", t);
}
if (offset > getBytesOccupied())
throw new IllegalArgumentException("Offset outside of mapped file range");
if (length > -1 && length + offset > getBytesOccupied())
throw new IllegalArgumentException("Offset / length outside of mapped file range");
return SENTINEL_BUFFER;
int beginIndex = Math.toIntExact(offset / maxBufferSize);
int firstOffset = Math.toIntExact(offset % maxBufferSize);
int endIndex = calculateEndIndex(offset, length);
int lastLen = calculateLastLen(offset, length);
new IteratingNestedCallback(callback)
{
int index = beginIndex;
@Override
protected Action process()
{
if (index > endIndex)
return Action.SUCCEEDED;
ByteBuffer currentBuffer = _buffers[index];
int offset = index == beginIndex ? firstOffset : 0;
int len = index == endIndex ? lastLen : -1;
boolean last = index == endIndex;
index++;
sink.write(last, BufferUtil.slice(currentBuffer, offset, len), this);
return Action.SCHEDULED;
}
}.iterate();
}
catch (Throwable x)
{
callback.failed(x);
}
}
private int calculateLastLen(long offset, long length)
{
if (length == 0)
return 0;
int lastLen = length < 0 ? -1 : Math.toIntExact((length + offset) % maxBufferSize);
if (Math.toIntExact((length + offset) / maxBufferSize) == _buffers.length)
lastLen = -1;
return lastLen;
}
private int calculateEndIndex(long offset, long length)
{
int endIndex = length < 0 ? (_buffers.length - 1) : Math.toIntExact((length + offset) / maxBufferSize);
if (endIndex == _buffers.length)
endIndex--;
return endIndex;
}
@Override
public long getBytesOccupied()
{
return _bytesOccupied;
}
}
}

View File

@ -14,13 +14,14 @@
package org.eclipse.jetty.http.content;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Set;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.MimeTypes.Type;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
/**
@ -61,15 +62,16 @@ public interface HttpContent
String getETagValue();
/**
* Get the {@link Resource} backing this HTTP content.
* @return the backing resource.
*/
Resource getResource();
/**
* <p>Get this HTTP content as a {@link ByteBuffer} if possible.</p>
* <p>Each invocation returns a new {@link ByteBuffer} instance that is
* read-only and contains valid data between the pos and the limit.</p>
* @return a {@link ByteBuffer} instance or null.
* <p>Write a subset of this HTTP content, to a {@link Content.Sink}.</p>
*/
ByteBuffer getByteBuffer();
void writeTo(Content.Sink sink, long offset, long length, Callback callback);
default long getBytesOccupied()
{
@ -93,7 +95,7 @@ public interface HttpContent
HttpContent getContent(String path) throws IOException;
}
// TODO add a writeTo semantic, then update IOResources to use a RBB.Dynamic
// TODO update IOResources to use a RBB.Dynamic
/**
* HttpContent Wrapper.
@ -197,9 +199,9 @@ public interface HttpContent
}
@Override
public ByteBuffer getByteBuffer()
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
return _delegate.getByteBuffer();
_delegate.writeTo(sink, offset, length, callback);
}
@Override

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.http.content;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Set;
@ -22,6 +21,8 @@ import org.eclipse.jetty.http.EtagUtils;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes.Type;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
public class PreCompressedHttpContent implements HttpContent
@ -142,9 +143,9 @@ public class PreCompressedHttpContent implements HttpContent
}
@Override
public ByteBuffer getByteBuffer()
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
return _precompressedContent.getByteBuffer();
_precompressedContent.writeTo(sink, offset, length, callback);
}
@Override

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.http.content;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Set;
@ -25,6 +24,10 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MimeTypes.Type;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
/**
@ -39,13 +42,15 @@ public class ResourceHttpContent implements HttpContent
final Path _path;
final String _contentType;
final HttpField _etag;
final ByteBufferPool.Sized _sizedBufferPool;
public ResourceHttpContent(final Resource resource, final String contentType)
public ResourceHttpContent(Resource resource, String contentType, ByteBufferPool.Sized sizedByteBufferPool)
{
_resource = resource;
_path = resource.getPath();
_contentType = contentType;
_etag = EtagUtils.createWeakEtagField(resource);
_sizedBufferPool = sizedByteBufferPool;
}
@Override
@ -144,9 +149,9 @@ public class ResourceHttpContent implements HttpContent
}
@Override
public ByteBuffer getByteBuffer()
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
return null;
IOResources.copy(_resource, sink, _sizedBufferPool, _sizedBufferPool.getSize(), _sizedBufferPool.isDirect(), offset, length, callback);
}
@Override

View File

@ -18,6 +18,7 @@ import java.nio.file.InvalidPathException;
import java.util.Objects;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.resource.Resources;
@ -31,12 +32,14 @@ public class ResourceHttpContentFactory implements HttpContent.Factory
{
private final Resource _baseResource;
private final MimeTypes _mimeTypes;
private final ByteBufferPool.Sized _sizedBufferPool;
public ResourceHttpContentFactory(Resource baseResource, MimeTypes mimeTypes)
public ResourceHttpContentFactory(Resource baseResource, MimeTypes mimeTypes, ByteBufferPool.Sized sizedBufferPool)
{
Objects.requireNonNull(mimeTypes, "MimeTypes cannot be null");
_baseResource = Objects.requireNonNullElse(baseResource, ResourceFactory.root().newResource("."));
_mimeTypes = mimeTypes;
_sizedBufferPool = sizedBufferPool;
}
@Override
@ -73,7 +76,7 @@ public class ResourceHttpContentFactory implements HttpContent.Factory
{
if (resource == null || !resource.exists())
return null;
return new ResourceHttpContent(resource, _mimeTypes.getMimeByExtension(pathInContext));
return new ResourceHttpContent(resource, _mimeTypes.getMimeByExtension(pathInContext), _sizedBufferPool);
}
@Override

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.http.content;
import java.io.IOException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,13 +33,15 @@ public class VirtualHttpContentFactory implements HttpContent.Factory
private final Resource _resource;
private final String _contentType;
private final String _matchSuffix;
private final ByteBufferPool.Sized _sizedBufferPool;
public VirtualHttpContentFactory(HttpContent.Factory factory, Resource resource, String contentType)
public VirtualHttpContentFactory(HttpContent.Factory factory, Resource resource, String contentType, ByteBufferPool.Sized sizedBufferPool)
{
_factory = factory;
_resource = resource;
_matchSuffix = "/" + _resource.getFileName();
_contentType = contentType;
_sizedBufferPool = sizedBufferPool;
if (LOG.isDebugEnabled())
LOG.debug("resource=({}) {}, resource.getFileName()={}", _resource.getClass().getName(), _resource, _resource.getFileName());
}
@ -58,7 +61,7 @@ public class VirtualHttpContentFactory implements HttpContent.Factory
if (content != null)
return content;
if (matchResource(path))
return new ResourceHttpContent(_resource, _contentType);
return new ResourceHttpContent(_resource, _contentType, _sizedBufferPool);
return null;
}

View File

@ -0,0 +1,89 @@
//
// ========================================================================
// 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.http.content;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(WorkDirExtension.class)
public class FileMappingHttpContentFactoryTest
{
public WorkDir workDir;
@Test
public void testMultiBufferFileMapped() throws Exception
{
Path file = Files.writeString(workDir.getEmptyPathDir().resolve("file.txt"), "0123456789abcdefghijABCDEFGHIJ");
FileMappingHttpContentFactory fileMappingHttpContentFactory = new FileMappingHttpContentFactory(
new ResourceHttpContentFactory(ResourceFactory.root().newResource(file.getParent()), MimeTypes.DEFAULTS, ByteBufferPool.SIZED_NON_POOLING),
0, 10);
HttpContent content = fileMappingHttpContentFactory.getContent("file.txt");
assertThrows(IllegalArgumentException.class, () -> writeToString(content, 0, 31));
assertThrows(IllegalArgumentException.class, () -> writeToString(content, 30, 1));
assertThrows(IllegalArgumentException.class, () -> writeToString(content, 31, 0));
assertThat(writeToString(content, 0, 30), is("0123456789abcdefghijABCDEFGHIJ"));
assertThat(writeToString(content, 29, 1), is("J"));
assertThat(writeToString(content, 0, 0), is(""));
assertThat(writeToString(content, 10, 0), is(""));
assertThat(writeToString(content, 15, 0), is(""));
assertThat(writeToString(content, 20, 0), is(""));
assertThat(writeToString(content, 30, 0), is(""));
assertThat(writeToString(content, 1, 28), is("123456789abcdefghijABCDEFGHI"));
assertThat(writeToString(content, 0, 10), is("0123456789"));
assertThat(writeToString(content, 10, 10), is("abcdefghij"));
assertThat(writeToString(content, 20, 10), is("ABCDEFGHIJ"));
assertThat(writeToString(content, 5, 10), is("56789abcde"));
assertThat(writeToString(content, 15, 10), is("fghijABCDE"));
assertThat(writeToString(content, 25, 5), is("FGHIJ"));
assertThat(writeToString(content, 0, -1), is("0123456789abcdefghijABCDEFGHIJ"));
assertThat(writeToString(content, 5, -1), is("56789abcdefghijABCDEFGHIJ"));
assertThat(writeToString(content, 10, -1), is("abcdefghijABCDEFGHIJ"));
assertThat(writeToString(content, 15, -1), is("fghijABCDEFGHIJ"));
assertThat(writeToString(content, 20, -1), is("ABCDEFGHIJ"));
assertThat(writeToString(content, 25, -1), is("FGHIJ"));
}
private static String writeToString(HttpContent content, long offset, long length) throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (Blocker.Callback cb = Blocker.callback())
{
content.writeTo(Content.Sink.from(baos), offset, length, cb);
cb.block();
}
return baos.toString(StandardCharsets.UTF_8);
}
}

View File

@ -267,29 +267,7 @@ public class Content
*/
static Content.Source from(ByteBufferPool.Sized byteBufferPool, InputStream inputStream, long offset, long length)
{
return new InputStreamContentSource(inputStream, byteBufferPool)
{
private long skip = offset;
private long toRead = length;
@Override
protected int fillBufferFromInputStream(InputStream inputStream, byte[] buffer) throws IOException
{
if (skip > 0)
{
inputStream.skipNBytes(skip);
skip = 0;
}
if (toRead == 0)
return -1;
int toReadInt = (int)Math.min(Integer.MAX_VALUE, toRead);
int len = toReadInt > -1 ? Math.min(toReadInt, buffer.length) : buffer.length;
int read = inputStream.read(buffer, 0, len);
toRead -= read;
return read;
}
};
return new InputStreamContentSource(inputStream, byteBufferPool, offset, length);
}
/**

View File

@ -31,6 +31,7 @@ import org.eclipse.jetty.util.resource.Resource;
/**
* Common IO operations for {@link Resource} content.
* // TODO use ByteBufferPool.Sized instead of ByteBufferPool in all signatures.
*/
public class IOResources
{
@ -177,12 +178,12 @@ public class IOResources
* @param bufferPool the {@link ByteBufferPool} to get buffers from. null means allocate new buffers as needed.
* @param bufferSize the size of the buffer to be used for the copy. Any value &lt; 1 means use a default value.
* @param direct the directness of the buffers, this parameter is ignored if {@code bufferSize} is &lt; 1.
* @param first the first byte from which to read from.
* @param offset the first byte from which to read from.
* @param length the length of the content to read.
* @return the {@link Content.Source}.
* @throws IllegalArgumentException if the resource is a directory or does not exist or there is no way to access its contents.
*/
public static Content.Source asContentSource(Resource resource, ByteBufferPool bufferPool, int bufferSize, boolean direct, long first, long length) throws IllegalArgumentException
public static Content.Source asContentSource(Resource resource, ByteBufferPool bufferPool, int bufferSize, boolean direct, long offset, long length) throws IllegalArgumentException
{
if (resource.isDirectory() || !resource.exists())
throw new IllegalArgumentException("Resource must exist and cannot be a directory: " + resource);
@ -190,9 +191,7 @@ public class IOResources
// Try using the resource's path if possible, as the nio API is async and helps to avoid buffer copies.
Path path = resource.getPath();
if (path != null)
{
return Content.Source.from(new ByteBufferPool.Sized(bufferPool, direct, bufferSize), path, first, length);
}
return Content.Source.from(new ByteBufferPool.Sized(bufferPool, direct, bufferSize), path, offset, length);
// Try an optimization for MemoryResource.
if (resource instanceof MemoryResource memoryResource)
@ -204,7 +203,7 @@ public class IOResources
InputStream inputStream = resource.newInputStream();
if (inputStream == null)
throw new IllegalArgumentException("Resource does not support InputStream: " + resource);
return Content.Source.from(new ByteBufferPool.Sized(bufferPool, direct, bufferSize), inputStream, first, length);
return Content.Source.from(new ByteBufferPool.Sized(bufferPool, direct, bufferSize), inputStream, offset, length);
}
catch (IOException e)
{
@ -254,35 +253,35 @@ public class IOResources
*/
public static void copy(Resource resource, Content.Sink sink, ByteBufferPool bufferPool, int bufferSize, boolean direct, Callback callback) throws IllegalArgumentException
{
if (resource.isDirectory() || !resource.exists())
throw new IllegalArgumentException("Resource must exist and cannot be a directory: " + resource);
// Save a Content.Source allocation for resources with a Path.
Path path = resource.getPath();
if (path != null)
try
{
try
if (resource.isDirectory() || !resource.exists())
throw new IllegalArgumentException("Resource must exist and cannot be a directory: " + resource);
// Save a Content.Source allocation for resources with a Path.
Path path = resource.getPath();
if (path != null)
{
new PathToSinkCopier(path, sink, bufferPool, bufferSize, direct, callback).iterate();
return;
}
catch (Throwable x)
// Directly write the byte array if the resource is a MemoryResource.
if (resource instanceof MemoryResource memoryResource)
{
callback.failed(x);
byte[] bytes = memoryResource.getBytes();
sink.write(true, ByteBuffer.wrap(bytes), callback);
return;
}
return;
}
// Directly write the byte array if the resource is a MemoryResource.
if (resource instanceof MemoryResource memoryResource)
// Fallback to Content.Source.
Content.Source source = asContentSource(resource, bufferPool, bufferSize, direct);
Content.copy(source, sink, callback);
}
catch (Throwable x)
{
byte[] bytes = memoryResource.getBytes();
sink.write(true, ByteBuffer.wrap(bytes), callback);
return;
callback.failed(x);
}
// Fallback to Content.Source.
Content.Source source = asContentSource(resource, bufferPool, bufferSize, direct);
Content.copy(source, sink, callback);
}
/**
@ -304,40 +303,35 @@ public class IOResources
*/
public static void copy(Resource resource, Content.Sink sink, ByteBufferPool bufferPool, int bufferSize, boolean direct, long first, long length, Callback callback) throws IllegalArgumentException
{
if (resource.isDirectory() || !resource.exists())
throw new IllegalArgumentException("Resource must exist and cannot be a directory: " + resource);
// Save a Content.Source allocation for resources with a Path.
Path path = resource.getPath();
if (path != null)
try
{
try
if (resource.isDirectory() || !resource.exists())
throw new IllegalArgumentException("Resource must exist and cannot be a directory: " + resource);
// Save a Content.Source allocation for resources with a Path.
Path path = resource.getPath();
if (path != null)
{
new PathToSinkCopier(path, sink, bufferPool, bufferSize, direct, first, length, callback).iterate();
return;
}
catch (Throwable x)
// Directly write the byte array if the resource is a MemoryResource.
if (resource instanceof MemoryResource memoryResource)
{
callback.failed(x);
ByteBuffer byteBuffer = BufferUtil.slice(ByteBuffer.wrap(memoryResource.getBytes()), Math.toIntExact(first), Math.toIntExact(length));
sink.write(true, byteBuffer, callback);
return;
}
return;
}
// Directly write the byte array if the resource is a MemoryResource.
if (resource instanceof MemoryResource memoryResource)
// Fallback to Content.Source.
Content.Source source = asContentSource(resource, bufferPool, bufferSize, direct, first, length);
Content.copy(source, sink, callback);
}
catch (Throwable x)
{
byte[] bytes = memoryResource.getBytes();
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
if (first >= 0)
byteBuffer.position((int)first);
if (length >= 0)
byteBuffer.limit((int)(byteBuffer.position() + length));
sink.write(true, byteBuffer, callback);
return;
callback.failed(x);
}
// Fallback to Content.Source.
Content.Source source = asContentSource(resource, bufferPool, bufferSize, direct, first, length);
Content.copy(source, sink, callback);
}
private static class PathToSinkCopier extends IteratingNestedCallback
@ -356,12 +350,16 @@ public class IOResources
this(path, sink, pool, bufferSize, direct, -1L, -1L, callback);
}
public PathToSinkCopier(Path path, Content.Sink sink, ByteBufferPool pool, int bufferSize, boolean direct, long first, long length, Callback callback) throws IOException
public PathToSinkCopier(Path path, Content.Sink sink, ByteBufferPool pool, int bufferSize, boolean direct, long offset, long length, Callback callback) throws IOException
{
super(callback);
this.channel = Files.newByteChannel(path);
if (first > -1)
channel.position(first);
if (offset > -1)
{
if (offset > channel.size() && length != 0)
throw new IllegalArgumentException("Offset outside of Path range");
channel.position(offset);
}
this.sink = sink;
this.pool = pool == null ? ByteBufferPool.NON_POOLING : pool;
this.bufferSize = bufferSize <= 0 ? 4096 : bufferSize;

View File

@ -21,6 +21,7 @@ import java.util.Objects;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.AutoLock;
@ -40,65 +41,55 @@ public class InputStreamContentSource implements Content.Source
private final AutoLock lock = new AutoLock();
private final SerializedInvoker invoker = new SerializedInvoker();
private final InputStream inputStream;
private ByteBufferPool.Sized bufferPool;
private final ByteBufferPool.Sized bufferPool;
private Runnable demandCallback;
private Content.Chunk errorChunk;
private long toRead;
private boolean closed;
/**
* @deprecated Use {@link #InputStreamContentSource(InputStream, ByteBufferPool.Sized)} instead.
*/
@Deprecated
public InputStreamContentSource(InputStream inputStream)
{
this(inputStream, null);
}
public InputStreamContentSource(InputStream inputStream, ByteBufferPool bufferPool)
{
this(inputStream, bufferPool instanceof ByteBufferPool.Sized sized ? sized : new ByteBufferPool.Sized(bufferPool));
}
public InputStreamContentSource(InputStream inputStream, ByteBufferPool.Sized bufferPool)
{
this(inputStream, bufferPool, 0L, -1L);
}
public InputStreamContentSource(InputStream inputStream, ByteBufferPool.Sized bufferPool, long offset, long length)
{
this.inputStream = Objects.requireNonNull(inputStream);
this.bufferPool = Objects.requireNonNullElse(bufferPool, ByteBufferPool.SIZED_NON_POOLING);
bufferPool = Objects.requireNonNullElse(bufferPool, ByteBufferPool.SIZED_NON_POOLING);
// Make sure direct is always false as the implementation requires heap buffers to be able to call array().
if (bufferPool.isDirect())
bufferPool = new ByteBufferPool.Sized(bufferPool.getWrapped(), false, bufferPool.getSize());
this.bufferPool = bufferPool;
skipToOffset(inputStream, offset, length);
this.toRead = length;
}
public int getBufferSize()
private static void skipToOffset(InputStream inputStream, long offset, long length)
{
return bufferPool.getSize();
}
/**
* @param bufferSize The size of the buffer
* @deprecated Use {@link InputStreamContentSource#InputStreamContentSource(InputStream, ByteBufferPool.Sized)}
*/
@Deprecated(forRemoval = true)
public void setBufferSize(int bufferSize)
{
try (AutoLock ignored = lock.lock())
if (offset > 0L && length != 0L)
{
if (bufferSize != bufferPool.getSize())
bufferPool = new ByteBufferPool.Sized(bufferPool.getWrapped(), bufferPool.isDirect(), bufferSize);
try
{
inputStream.skip(offset - 1);
if (inputStream.read() == -1)
throw new IllegalArgumentException("Offset out of range");
}
catch (IOException e)
{
throw new RuntimeIOException(e);
}
}
}
public boolean isUseDirectByteBuffers()
{
return bufferPool.isDirect();
}
/**
* @param useDirectByteBuffers {@code true} if direct buffers will be used.
* @deprecated Use {@link InputStreamContentSource#InputStreamContentSource(InputStream, ByteBufferPool.Sized)}
*/
@Deprecated(forRemoval = true, since = "12.0.11")
public void setUseDirectByteBuffers(boolean useDirectByteBuffers)
{
try (AutoLock ignored = lock.lock())
{
if (useDirectByteBuffers != bufferPool.isDirect())
bufferPool = new ByteBufferPool.Sized(bufferPool.getWrapped(), useDirectByteBuffers, bufferPool.getSize());
}
}
@Override
public Content.Chunk read()
{
@ -136,7 +127,13 @@ public class InputStreamContentSource implements Content.Source
protected int fillBufferFromInputStream(InputStream inputStream, byte[] buffer) throws IOException
{
return inputStream.read(buffer, 0, buffer.length);
if (toRead == 0L)
return -1;
int toReadInt = toRead >= Integer.MAX_VALUE || toRead < 0L ? -1 : (int)toRead;
int len = toReadInt > -1 ? Math.min(toReadInt, buffer.length) : buffer.length;
int read = inputStream.read(buffer, 0, len);
toRead -= read;
return read;
}
private void close()

View File

@ -18,6 +18,7 @@ import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
@ -29,6 +30,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class IOResourcesTest
{
@ -179,4 +181,28 @@ public class IOResourcesTest
assertThat(sum, is(500L));
assertThat(chunks.get(chunks.size() - 1).isLast(), is(true));
}
@ParameterizedTest
@MethodSource("all")
public void testOutOfRangeOffset(Resource resource)
{
TestSink sink = new TestSink();
Blocker.Callback callback = Blocker.callback();
IOResources.copy(resource, sink, bufferPool, 1, false, Integer.MAX_VALUE, 1, callback);
assertThrows(IllegalArgumentException.class, callback::block);
}
@ParameterizedTest
@MethodSource("all")
public void testOutOfRangeOffsetWithZeroLength(Resource resource) throws Exception
{
TestSink sink = new TestSink();
Callback.Completable callback = new Callback.Completable();
IOResources.copy(resource, sink, bufferPool, 1, false, Integer.MAX_VALUE, 0, callback);
callback.get();
List<Content.Chunk> chunks = sink.takeAccumulatedChunks();
long sum = chunks.stream().mapToLong(Content.Chunk::remaining).sum();
assertThat(sum, is(0L));
assertThat(chunks.get(chunks.size() - 1).isLast(), is(true));
}
}

View File

@ -43,7 +43,6 @@ 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.Content;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.URIUtil;
@ -659,7 +658,7 @@ public class ResourceService
putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH);
else
putHeaders(response, content, NO_CONTENT_LENGTH);
writeHttpContent(request, response, callback, content);
content.writeTo(response, 0L, -1L, callback);
return;
}
@ -681,9 +680,7 @@ public class ResourceService
putHeaders(response, content, range.getLength());
response.setStatus(HttpStatus.PARTIAL_CONTENT_206);
response.getHeaders().put(HttpHeader.CONTENT_RANGE, range.toHeaderValue(contentLength));
// TODO use a buffer pool
IOResources.copy(content.getResource(), response, null, 0, false, range.first(), range.getLength(), callback);
content.writeTo(response, range.first(), range.getLength(), callback);
return;
}
@ -700,32 +697,6 @@ public class ResourceService
Content.copy(byteRanges, response, callback);
}
protected void writeHttpContent(Request request, Response response, Callback callback, HttpContent content)
{
try
{
ByteBuffer buffer = content.getByteBuffer(); // this buffer is going to be consumed by response.write()
if (buffer != null)
{
response.write(true, buffer, callback);
}
else
{
IOResources.copy(
content.getResource(),
response, request.getComponents().getByteBufferPool(),
request.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(),
request.getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(),
callback);
}
}
catch (Throwable x)
{
content.release();
callback.failed(x);
}
}
protected void putHeaders(Response response, HttpContent content, long contentLength)
{
// TODO it is very inefficient to do many put's to a HttpFields, as each put is a full iteration.

View File

@ -54,9 +54,11 @@ public class ResourceHandler extends Handler.Wrapper
// - request ranges
// - a way to configure caching or not
private static final Logger LOG = LoggerFactory.getLogger(ResourceHandler.class);
private static final int DEFAULT_BUFFER_SIZE = 32768;
private static final boolean DEFAULT_USE_DIRECT_BUFFERS = true;
private final ResourceService _resourceService = newResourceService();
private ByteBufferPool _byteBufferPool;
private ByteBufferPool.Sized _byteBufferPool;
private Resource _baseResource;
private Resource _styleSheet;
private MimeTypes _mimeTypes;
@ -65,12 +67,18 @@ public class ResourceHandler extends Handler.Wrapper
public ResourceHandler()
{
this(null);
this(null, null);
}
public ResourceHandler(Handler handler)
{
this(handler, null);
}
public ResourceHandler(Handler handler, ByteBufferPool.Sized byteBufferPool)
{
super(handler);
_byteBufferPool = byteBufferPool;
}
protected ResourceService newResourceService()
@ -99,9 +107,10 @@ public class ResourceHandler extends Handler.Wrapper
setMimeTypes(context == null ? MimeTypes.DEFAULTS : context.getMimeTypes());
_byteBufferPool = getByteBufferPool(context);
if (_byteBufferPool == null)
_byteBufferPool = new ByteBufferPool.Sized(findByteBufferPool(), DEFAULT_USE_DIRECT_BUFFERS, DEFAULT_BUFFER_SIZE);
ResourceService resourceService = getResourceService();
resourceService.setHttpContentFactory(newHttpContentFactory());
resourceService.setHttpContentFactory(newHttpContentFactory(_byteBufferPool));
resourceService.setWelcomeFactory(setupWelcomeFactory());
if (getStyleSheet() == null)
setStyleSheet(getServer().getDefaultStyleSheet());
@ -109,10 +118,8 @@ public class ResourceHandler extends Handler.Wrapper
super.doStart();
}
private ByteBufferPool getByteBufferPool(Context context)
private ByteBufferPool findByteBufferPool()
{
if (context == null)
return ByteBufferPool.NON_POOLING;
Server server = getServer();
if (server == null)
return ByteBufferPool.NON_POOLING;
@ -124,14 +131,14 @@ public class ResourceHandler extends Handler.Wrapper
return _resourceService.getHttpContentFactory();
}
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(getBaseResource(), getMimeTypes());
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(getBaseResource(), getMimeTypes(), byteBufferPool);
if (isUseFileMapping())
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, getStyleSheet(), "text/css");
contentFactory = new VirtualHttpContentFactory(contentFactory, getStyleSheet(), "text/css", byteBufferPool);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, getPrecompressedFormats());
contentFactory = new ValidatingCachingHttpContentFactory(contentFactory, Duration.ofSeconds(1).toMillis(), getByteBufferPool());
contentFactory = new ValidatingCachingHttpContentFactory(contentFactory, Duration.ofSeconds(1).toMillis(), byteBufferPool);
return contentFactory;
}
@ -182,11 +189,6 @@ public class ResourceHandler extends Handler.Wrapper
return _baseResource;
}
public ByteBufferPool getByteBufferPool()
{
return _byteBufferPool;
}
/**
* Get the cacheControl header to set on all static content..
* @return the cacheControl header to set on all static content.

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.server.handler;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
@ -30,7 +29,6 @@ import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.ResourceService;
@ -104,9 +102,9 @@ public class ResourceHandlerByteRangesTest
final Resource memResource = ResourceFactory.of(this).newMemoryResource(getClass().getResource("/simple/big.txt"));
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
return path -> new ResourceHttpContent(memResource, "text/plain");
return path -> new ResourceHttpContent(memResource, "text/plain", byteBufferPool);
}
});
@ -136,9 +134,9 @@ public class ResourceHandlerByteRangesTest
final Resource memResource = ResourceFactory.of(this).newMemoryResource(getClass().getResource("/simple/big.txt"));
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
return path -> new ResourceHttpContent(memResource, "text/plain");
return path -> new ResourceHttpContent(memResource, "text/plain", byteBufferPool);
}
});
@ -170,18 +168,9 @@ public class ResourceHandlerByteRangesTest
final Resource memResource = ResourceFactory.of(this).newMemoryResource(getClass().getResource("/simple/big.txt"));
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
return path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
};
return path -> new ResourceHttpContent(memResource, "text/plain", byteBufferPool);
}
});
@ -211,18 +200,9 @@ public class ResourceHandlerByteRangesTest
final Resource memResource = ResourceFactory.of(this).newMemoryResource(getClass().getResource("/simple/big.txt"));
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
return path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
};
return path -> new ResourceHttpContent(memResource, "text/plain", byteBufferPool);
}
});
@ -254,9 +234,9 @@ public class ResourceHandlerByteRangesTest
final Resource memResource = ResourceFactory.of(this).newMemoryResource(getClass().getResource("/simple/big.txt"));
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
return path -> new ResourceHttpContent(memResource, "text/plain");
return path -> new ResourceHttpContent(memResource, "text/plain", byteBufferPool);
}
@Override

View File

@ -49,6 +49,7 @@ import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory;
import org.eclipse.jetty.http.content.VirtualHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.Handler;
@ -664,13 +665,13 @@ public class ResourceHandlerTest
_rootResourceHandler = new ResourceHandler()
{
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(getBaseResource(), getMimeTypes());
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(getBaseResource(), getMimeTypes(), byteBufferPool);
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, getStyleSheet(), "text/css");
contentFactory = new VirtualHttpContentFactory(contentFactory, getStyleSheet(), "text/css", byteBufferPool);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, getPrecompressedFormats());
contentFactory = new ValidatingCachingHttpContentFactory(contentFactory, 0, getByteBufferPool());
contentFactory = new ValidatingCachingHttpContentFactory(contentFactory, 0, byteBufferPool);
return contentFactory;
}
};

View File

@ -29,6 +29,7 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
@ -100,10 +101,10 @@ public class TryPathsHandlerTest
ResourceHandler resourceHandler = new ResourceHandler()
{
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
// We don't want to cache not found entries for this test.
return new ResourceHttpContentFactory(getBaseResource(), getMimeTypes());
return new ResourceHttpContentFactory(getBaseResource(), getMimeTypes(), byteBufferPool);
}
};
@ -171,10 +172,10 @@ public class TryPathsHandlerTest
ResourceHandler resourceHandler = new ResourceHandler()
{
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
// We don't want to cache not found entries for this test.
return new ResourceHttpContentFactory(getBaseResource(), getMimeTypes());
return new ResourceHttpContentFactory(getBaseResource(), getMimeTypes(), byteBufferPool);
}
};
resourceHandler.setDirAllowed(false);
@ -299,10 +300,10 @@ public class TryPathsHandlerTest
ResourceHandler resourceHandler = new ResourceHandler()
{
@Override
protected HttpContent.Factory newHttpContentFactory()
protected HttpContent.Factory newHttpContentFactory(ByteBufferPool.Sized byteBufferPool)
{
// We don't want to cache not found entries for this test.
return new ResourceHttpContentFactory(getBaseResource(), getMimeTypes());
return new ResourceHttpContentFactory(getBaseResource(), getMimeTypes(), byteBufferPool);
}
};
resourceHandler.setDirAllowed(false);

View File

@ -287,6 +287,23 @@ public class BufferUtil
flipToFlush(buffer, p);
}
/**
* Slice a buffer given an offset and a length.
* @param buffer the buffer to slice
* @param offset the offset
* @param length the length, -1 meaning use the current limit
* @return the sliced buffer
*/
public static ByteBuffer slice(ByteBuffer buffer, int offset, int length)
{
ByteBuffer slice = buffer.slice();
if (offset > 0)
slice.position(slice.position() + offset);
if (length > -1)
slice.limit(slice.position() + length);
return slice;
}
/**
* Convert a ByteBuffer to a byte array.
*

View File

@ -17,6 +17,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
@ -57,6 +58,22 @@ public class BufferUtilTest
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
@Test
public void testSlice()
{
ByteBuffer byteBuffer = ByteBuffer.wrap("0123456789".getBytes(StandardCharsets.UTF_8));
assertEquals("0123456789", BufferUtil.toString(BufferUtil.slice(byteBuffer, 0, -1)));
assertEquals("3456789", BufferUtil.toString(BufferUtil.slice(byteBuffer, 3, -1)));
assertEquals("01234567", BufferUtil.toString(BufferUtil.slice(byteBuffer, 0, 8)));
assertEquals("5678", BufferUtil.toString(BufferUtil.slice(byteBuffer, 5, 4)));
assertEquals("", BufferUtil.toString(BufferUtil.slice(byteBuffer, 1, 0)));
assertEquals("", BufferUtil.toString(BufferUtil.slice(byteBuffer, 10, -1)));
assertThrows(IllegalArgumentException.class, () -> BufferUtil.slice(byteBuffer, 0, 11));
assertThrows(IllegalArgumentException.class, () -> BufferUtil.slice(byteBuffer, 11, -1));
assertThrows(IllegalArgumentException.class, () -> BufferUtil.slice(byteBuffer, 1, 10));
}
@Test
public void testToInt() throws Exception
{

View File

@ -149,9 +149,20 @@ import org.slf4j.LoggerFactory;
* Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
* The path of a custom stylesheet to style the directory listing HTML.
* </dd>
* <dt>byteBufferSize</dt>
* <dd>
* The size of the buffers to use to serve static resources.
* Defaults to {@code 32 KiB}.
* </dd>
* <dt>useDirectByteBuffers</dt>
* <dd>
* Use {@code true} to use direct byte buffers to serve static resources.
* Defaults to {@code true}.
* </dd>
* <dt>useFileMappedBuffer</dt>
* <dd>
* Use {@code true} to use file mapping to serve static resources.
* Use {@code true} to use file mapping to serve static resources instead of
* buffers configured with the above two settings.
* Defaults to {@code false}.
* </dd>
* <dt>welcomeServlets</dt>
@ -214,7 +225,9 @@ public class ResourceServlet extends HttpServlet
if (contentFactory == null)
{
MimeTypes mimeTypes = contextHandler.getMimeTypes();
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes);
ByteBufferPool bufferPool = getByteBufferPool(contextHandler);
ByteBufferPool.Sized sizedBufferPool = new ByteBufferPool.Sized(bufferPool, getInitBoolean("useDirectByteBuffers", true), getInitInt("byteBufferSize", 32768));
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes, sizedBufferPool);
// Use the servers default stylesheet unless there is one explicitly set by an init param.
Resource styleSheet = contextHandler.getServer().getDefaultStyleSheet();
@ -242,7 +255,7 @@ public class ResourceServlet extends HttpServlet
if (getInitBoolean("useFileMappedBuffer", false))
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css");
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css", sizedBufferPool);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, precompressedFormats);
int maxCacheSize = getInitInt("maxCacheSize", -2);
@ -251,7 +264,6 @@ public class ResourceServlet extends HttpServlet
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{
ByteBufferPool bufferPool = getByteBufferPool(contextHandler);
ValidatingCachingHttpContentFactory cached = new ValidatingCachingHttpContentFactory(contentFactory,
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
contentFactory = cached;

View File

@ -121,10 +121,9 @@ public class ServletMultiPartFormData
}
else
{
// TODO use the size specified in ByteBufferPool.SIZED_NON_POOLING instead of specifying a 2K buffer size?
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
InputStreamContentSource iscs = new InputStreamContentSource(servletRequest.getInputStream(), byteBufferPool);
iscs.setBufferSize(bufferSize);
source = iscs;
source = new InputStreamContentSource(servletRequest.getInputStream(), new ByteBufferPool.Sized(byteBufferPool, false, bufferSize));
}
MultiPartConfig multiPartConfig = Request.getMultiPartConfig(servletContextRequest, filesDirectory)

View File

@ -20,7 +20,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
@ -54,7 +53,6 @@ import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.HttpConfiguration;
@ -3508,7 +3506,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3529,16 +3527,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3559,7 +3548,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3583,16 +3572,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3616,7 +3596,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
defaultServlet.getResourceService().setAcceptRanges(false);
String rawResponse = connector.getResponse("""

View File

@ -21,7 +21,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
@ -59,7 +58,6 @@ import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.HttpConfiguration;
@ -3646,7 +3644,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3667,16 +3665,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3697,7 +3686,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3721,16 +3710,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3754,7 +3734,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
resourceServlet.getResourceService().setAcceptRanges(false);
String rawResponse = connector.getResponse("""

View File

@ -149,9 +149,20 @@ import org.slf4j.LoggerFactory;
* Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
* The path of a custom stylesheet to style the directory listing HTML.
* </dd>
* <dt>byteBufferSize</dt>
* <dd>
* The size of the buffers to use to serve static resources.
* Defaults to {@code 32 KiB}.
* </dd>
* <dt>useDirectByteBuffers</dt>
* <dd>
* Use {@code true} to use direct byte buffers to serve static resources.
* Defaults to {@code true}.
* </dd>
* <dt>useFileMappedBuffer</dt>
* <dd>
* Use {@code true} to use file mapping to serve static resources.
* Use {@code true} to use file mapping to serve static resources instead of
* buffers configured with the above two settings.
* Defaults to {@code false}.
* </dd>
* <dt>welcomeServlets</dt>
@ -214,7 +225,9 @@ public class ResourceServlet extends HttpServlet
if (contentFactory == null)
{
MimeTypes mimeTypes = contextHandler.getMimeTypes();
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes);
ByteBufferPool bufferPool = getByteBufferPool(contextHandler);
ByteBufferPool.Sized sizedBufferPool = new ByteBufferPool.Sized(bufferPool, getInitBoolean("useDirectByteBuffers", true), getInitInt("byteBufferSize", 32768));
contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes, sizedBufferPool);
// Use the servers default stylesheet unless there is one explicitly set by an init param.
Resource styleSheet = contextHandler.getServer().getDefaultStyleSheet();
@ -242,7 +255,7 @@ public class ResourceServlet extends HttpServlet
if (getInitBoolean("useFileMappedBuffer", false))
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css");
contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css", sizedBufferPool);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, precompressedFormats);
int maxCacheSize = getInitInt("maxCacheSize", -2);
@ -251,7 +264,6 @@ public class ResourceServlet extends HttpServlet
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{
ByteBufferPool bufferPool = getByteBufferPool(contextHandler);
ValidatingCachingHttpContentFactory cached = new ValidatingCachingHttpContentFactory(contentFactory,
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
contentFactory = cached;

View File

@ -125,10 +125,9 @@ public class ServletMultiPartFormData
}
else
{
// TODO use the size specified in ByteBufferPool.SIZED_NON_POOLING instead of specifying a 2K buffer size?
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
InputStreamContentSource iscs = new InputStreamContentSource(servletRequest.getInputStream(), byteBufferPool);
iscs.setBufferSize(bufferSize);
source = iscs;
source = new InputStreamContentSource(servletRequest.getInputStream(), new ByteBufferPool.Sized(byteBufferPool, false, bufferSize));
}
parser.setMaxParts(contextHandler.getMaxFormKeys());

View File

@ -20,7 +20,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
@ -54,7 +53,6 @@ import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.HttpConfiguration;
@ -3508,7 +3506,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3529,16 +3527,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3559,7 +3548,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3583,16 +3572,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3616,7 +3596,7 @@ public class DefaultServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
DefaultServlet defaultServlet = new DefaultServlet();
context.addServlet(new ServletHolder(defaultServlet), "/");
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
defaultServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
defaultServlet.getResourceService().setAcceptRanges(false);
String rawResponse = connector.getResponse("""

View File

@ -21,7 +21,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
@ -59,7 +58,6 @@ import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.HttpConfiguration;
@ -3646,7 +3644,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3667,16 +3665,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3697,7 +3686,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3721,16 +3710,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), ByteBufferPool.NON_POOLING, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
@ -3754,7 +3734,7 @@ public class ResourceServletTest
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceServlet resourceServlet = new ResourceServlet();
context.addServlet(new ServletHolder(resourceServlet), "/");
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceServlet.getResourceService().setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
resourceServlet.getResourceService().setAcceptRanges(false);
String rawResponse = connector.getResponse("""

View File

@ -1357,12 +1357,40 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{
if (LOG.isDebugEnabled())
LOG.debug("sendContent(http={},{})", httpContent, callback);
try
{
if (prepareSendContent(0, callback))
{
Content.Sink sink = (last, byteBuffer, cb) ->
{
_written += byteBuffer.remaining();
channelWrite(byteBuffer, last, cb);
};
httpContent.writeTo(sink, 0L, -1L, new Callback.Nested(callback)
{
@Override
public void succeeded()
{
onWriteComplete(true, null);
super.succeeded();
}
ByteBuffer buffer = httpContent.getByteBuffer();
if (buffer != null)
sendContent(buffer, callback);
else
sendContent(httpContent.getResource(), callback);
@Override
public void failed(Throwable x)
{
onWriteComplete(true, x);
super.failed(x);
}
});
}
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Unable to send http content {}", httpContent, x);
_channel.abort(x);
callback.failed(x);
}
}
private boolean prepareSendContent(int len, Callback callback)

View File

@ -52,7 +52,9 @@ public class ResourceHandler extends HandlerWrapper implements WelcomeFactory
{
private static final Logger LOG = LoggerFactory.getLogger(ResourceHandler.class);
private ByteBufferPool _bufferPool;
private ByteBufferPool.Sized _byteBufferPool;
private int _byteBufferSize = 32768;
private boolean _useDirectByteBuffers = true;
Resource _baseResource;
ContextHandler _context;
Resource _defaultStyleSheet;
@ -103,7 +105,7 @@ public class ResourceHandler extends HandlerWrapper implements WelcomeFactory
if (_mimeTypes == null)
_mimeTypes = _context == null ? MimeTypes.DEFAULTS : _context.getMimeTypes();
_bufferPool = getByteBufferPool(_context);
_byteBufferPool = new ByteBufferPool.Sized(getByteBufferPool(_context), _useDirectByteBuffers, _byteBufferSize);
if (_resourceService.getHttpContentFactory() == null)
_resourceService.setHttpContentFactory(newHttpContentFactory());
_resourceService.setWelcomeFactory(this);
@ -129,11 +131,11 @@ public class ResourceHandler extends HandlerWrapper implements WelcomeFactory
protected HttpContent.Factory newHttpContentFactory()
{
Resource baseResource = getBaseResource();
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(baseResource, _mimeTypes);
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(baseResource, _mimeTypes, _byteBufferPool);
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, getStyleSheet(), "text/css");
contentFactory = new VirtualHttpContentFactory(contentFactory, getStyleSheet(), "text/css", _byteBufferPool);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, _resourceService.getPrecompressedFormats());
contentFactory = new ValidatingCachingHttpContentFactory(contentFactory, Duration.ofSeconds(1).toMillis(), _bufferPool);
contentFactory = new ValidatingCachingHttpContentFactory(contentFactory, Duration.ofSeconds(1).toMillis(), _byteBufferPool);
return contentFactory;
}
@ -282,6 +284,22 @@ public class ResourceHandler extends HandlerWrapper implements WelcomeFactory
return _resourceService.isRedirectWelcome();
}
/**
* @return The size of the byte buffers used to serve static resources.
*/
public int getByteBufferSize()
{
return _byteBufferSize;
}
/**
* @return whether to use direct byte buffers to serve static resources.
*/
public boolean isUseDirectByteBuffers()
{
return _useDirectByteBuffers;
}
/**
* @param acceptRanges If true, range requests and responses are supported
*/
@ -385,6 +403,22 @@ public class ResourceHandler extends HandlerWrapper implements WelcomeFactory
_resourceService.setRedirectWelcome(redirectWelcome);
}
/**
* @param byteBufferSize The size of the byte buffers used to serve static resources.
*/
public void setByteBufferSize(int byteBufferSize)
{
_byteBufferSize = byteBufferSize;
}
/**
* @param useDirectByteBuffers whether to use direct byte buffers to serve static resources.
*/
public void setUseDirectByteBuffers(boolean useDirectByteBuffers)
{
_useDirectByteBuffers = useDirectByteBuffers;
}
/**
* @param resourceBase The base resource as a string.
* @deprecated use {@link #setBaseResource(Resource)}

View File

@ -15,13 +15,9 @@ package org.eclipse.jetty.ee9.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;
@ -36,9 +32,6 @@ import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee9.nested.resource.HttpContentRangeWriter;
import org.eclipse.jetty.ee9.nested.resource.RangeWriter;
import org.eclipse.jetty.ee9.nested.resource.SeekableByteChannelRangeWriter;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.EtagUtils;
import org.eclipse.jetty.http.HttpDateTime;
@ -50,12 +43,12 @@ 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.Content;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.ResourceListing;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
@ -855,15 +848,12 @@ public class ResourceService
length += CRLF + DASHDASH + BOUNDARY + DASHDASH + CRLF;
response.setContentLengthLong(length);
try (RangeWriter rangeWriter = HttpContentRangeWriter.newRangeWriter(content))
i = 0;
for (InclusiveByteRange ibr : ranges)
{
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.startPart(mimetype, new String[]{HttpHeader.CONTENT_RANGE + ": " + header[i]});
writeContent(content, multi, ibr.getFirst(), ibr.getSize());
i++;
}
multi.close();
@ -873,37 +863,28 @@ public class ResourceService
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)
try (Blocker.Callback blocker = Blocker.callback())
{
// no need to modify buffer pointers when whole content is requested
if (start != 0 || content.getResource().length() != contentLength)
// Do not use Content.Sink.from(out) as HttpContent.writeTo() may write a last Chunk
// which would then be converted to OutputStream.close(), and we do not want to
// close the OutputStream here;
// this happens because Content.copy() and IOResources.copy() assume that when they
// read a last Chunk from a Content.Source, it should be written as a last Chunk
// to the Content.Sink.
Content.Sink sink = (last, byteBuffer, callback) ->
{
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);
try
{
BufferUtil.writeTo(byteBuffer, out);
callback.succeeded();
}
catch (Throwable x)
{
callback.failed(x);
}
};
content.writeTo(sink, start, contentLength, blocker);
blocker.block();
}
}

View File

@ -1,57 +0,0 @@
//
// ========================================================================
// 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.ee9.nested.resource;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
/**
* ByteBuffer based RangeWriter
*/
public class ByteBufferRangeWriter implements RangeWriter
{
private final ByteBuffer buffer;
public ByteBufferRangeWriter(ByteBuffer buffer)
{
this.buffer = buffer;
}
@Override
public void close() throws IOException
{
}
@Override
public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
{
if (skipTo > Integer.MAX_VALUE)
{
throw new IllegalArgumentException("Unsupported skipTo " + skipTo + " > " + Integer.MAX_VALUE);
}
if (length > Integer.MAX_VALUE)
{
throw new IllegalArgumentException("Unsupported length " + skipTo + " > " + Integer.MAX_VALUE);
}
ByteBuffer src = buffer.slice();
src.position((int)skipTo);
src.limit(Math.addExact((int)skipTo, (int)length));
BufferUtil.writeTo(src, outputStream);
}
}

View File

@ -1,52 +0,0 @@
//
// ========================================================================
// 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.ee9.nested.resource;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.io.IOResources;
/**
* Range Writer selection for HttpContent
*/
public class HttpContentRangeWriter
{
/**
* Obtain a new RangeWriter for the supplied HttpContent.
*
* @param content the HttpContent to base RangeWriter on
* @return the RangeWriter best suited for the supplied HttpContent
*/
public static RangeWriter newRangeWriter(HttpContent content)
{
Objects.requireNonNull(content, "HttpContent");
// Try direct buffer
ByteBuffer buffer = content.getByteBuffer();
if (buffer != null)
return new ByteBufferRangeWriter(buffer);
// Try path's SeekableByteChannel
Path path = content.getResource().getPath();
if (path != null)
return new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(path));
// Fallback to InputStream
return new InputStreamRangeWriter(() -> IOResources.asInputStream(content.getResource()));
}
}

View File

@ -1,121 +0,0 @@
//
// ========================================================================
// 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.ee9.nested.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.jetty.util.IO;
/**
* Default Range Writer for InputStream
*/
public class InputStreamRangeWriter implements RangeWriter
{
public static final int NO_PROGRESS_LIMIT = 3;
public interface InputStreamSupplier
{
InputStream newInputStream() throws IOException;
}
private final InputStreamSupplier inputStreamSupplier;
private boolean closed = false;
private InputStream inputStream;
private long pos;
/**
* Create InputStreamRangeWriter
*
* @param inputStreamSupplier Supplier of the InputStream. If the stream needs to be regenerated, such as the next
* requested range being before the current position, then the current InputStream is closed and a new one obtained
* from this supplier.
*/
public InputStreamRangeWriter(InputStreamSupplier inputStreamSupplier)
{
this.inputStreamSupplier = inputStreamSupplier;
}
@Override
public void close() throws IOException
{
closed = true;
if (inputStream != null)
{
inputStream.close();
}
}
@Override
public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
{
if (closed)
{
throw new IOException("RangeWriter is closed");
}
if (inputStream == null)
{
inputStream = inputStreamSupplier.newInputStream();
pos = 0;
}
if (skipTo < pos)
{
inputStream.close();
inputStream = inputStreamSupplier.newInputStream();
pos = 0;
}
if (pos < skipTo)
{
long skipSoFar = pos;
long actualSkipped;
int noProgressLoopLimit = NO_PROGRESS_LIMIT;
// loop till we reach desired point, break out on lack of progress.
while (noProgressLoopLimit > 0 && skipSoFar < skipTo)
{
actualSkipped = inputStream.skip(skipTo - skipSoFar);
if (actualSkipped == 0)
{
noProgressLoopLimit--;
}
else if (actualSkipped > 0)
{
skipSoFar += actualSkipped;
noProgressLoopLimit = NO_PROGRESS_LIMIT;
}
else
{
// negative values means the stream was closed or reached EOF
// either way, we've hit a state where we can no longer
// fulfill the requested range write.
throw new IOException("EOF reached before InputStream skip destination");
}
}
if (noProgressLoopLimit <= 0)
{
throw new IOException("No progress made to reach InputStream skip position " + (skipTo - pos));
}
pos = skipTo;
}
// TODO this is very inefficient as copy() allocates a 64K buffer.
IO.copy(inputStream, outputStream, length);
pos += length;
}
}

View File

@ -1,33 +0,0 @@
//
// ========================================================================
// 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.ee9.nested.resource;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
/**
* Interface for writing sections (ranges) of a single resource (SeekableByteChannel, Resource, etc) to an outputStream.
*/
public interface RangeWriter extends Closeable
{
/**
* Write the specific range (start, size) to the outputStream.
*
* @param outputStream the stream to write to
* @param skipTo the offset / skip-to / seek-to / position in the resource to start the write from
* @param length the size of the section to write
*/
void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException;
}

View File

@ -1,161 +0,0 @@
//
// ========================================================================
// 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.ee9.nested.resource;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
public class SeekableByteChannelRangeWriter implements RangeWriter
{
public static final int NO_PROGRESS_LIMIT = 3;
public interface ChannelSupplier
{
SeekableByteChannel newSeekableByteChannel() throws IOException;
}
private final ChannelSupplier channelSupplier;
private final int bufSize;
private final ByteBuffer buffer;
private SeekableByteChannel channel;
private long pos;
private boolean defaultSeekMode = true;
public SeekableByteChannelRangeWriter(SeekableByteChannelRangeWriter.ChannelSupplier channelSupplier)
{
this(null, channelSupplier);
}
public SeekableByteChannelRangeWriter(SeekableByteChannel initialChannel, SeekableByteChannelRangeWriter.ChannelSupplier channelSupplier)
{
this.channel = initialChannel;
this.channelSupplier = channelSupplier;
this.bufSize = IO.bufferSize;
this.buffer = BufferUtil.allocate(this.bufSize);
}
@Override
public void close() throws IOException
{
if (this.channel != null)
{
this.channel.close();
}
}
@Override
public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
{
skipTo(skipTo);
// copy from channel to output stream
long readTotal = 0;
while (readTotal < length)
{
BufferUtil.clearToFill(buffer);
int size = (int)Math.min(bufSize, length - readTotal);
buffer.limit(size);
int readLen = channel.read(buffer);
BufferUtil.flipToFlush(buffer, 0);
BufferUtil.writeTo(buffer, outputStream);
readTotal += readLen;
pos += readLen;
}
}
private void skipTo(long skipTo) throws IOException
{
if (channel == null)
{
channel = channelSupplier.newSeekableByteChannel();
pos = 0;
}
if (defaultSeekMode)
{
try
{
if (channel.position() != skipTo)
{
channel.position(skipTo);
pos = skipTo;
return;
}
}
catch (UnsupportedOperationException e)
{
defaultSeekMode = false;
fallbackSkipTo(skipTo);
}
}
else
{
// Fallback mode
fallbackSkipTo(skipTo);
}
}
private void fallbackSkipTo(long skipTo) throws IOException
{
if (skipTo < pos)
{
channel.close();
channel = channelSupplier.newSeekableByteChannel();
pos = 0;
}
if (pos < skipTo)
{
long skipSoFar = pos;
long actualSkipped;
int noProgressLoopLimit = NO_PROGRESS_LIMIT;
// loop till we reach desired point, break out on lack of progress.
while (noProgressLoopLimit > 0 && skipSoFar < skipTo)
{
BufferUtil.clearToFill(buffer);
int len = (int)Math.min(bufSize, (skipTo - skipSoFar));
buffer.limit(len);
actualSkipped = channel.read(buffer);
if (actualSkipped == 0)
{
noProgressLoopLimit--;
}
else if (actualSkipped > 0)
{
skipSoFar += actualSkipped;
noProgressLoopLimit = NO_PROGRESS_LIMIT;
}
else
{
// negative values means the stream was closed or reached EOF
// either way, we've hit a state where we can no longer
// fulfill the requested range write.
throw new IOException("EOF reached before SeekableByteChannel skip destination");
}
}
if (noProgressLoopLimit <= 0)
{
throw new IOException("No progress made to reach SeekableByteChannel skip position " + (skipTo - pos));
}
pos = skipTo;
}
}
}

View File

@ -111,10 +111,14 @@ import org.slf4j.LoggerFactory;
* maxCachedFileSize The maximum size of a file to cache
* maxCachedFiles The maximum number of files to cache
*
* byteBufferSize
* The size of the buffers used to serve static content when using NIO connector.
* useDirectByteBuffers
* Whether to use direct buffers to serve static content when using NIO connector.
* useFileMappedBuffer
* If set to true, it will use mapped file buffer to serve static content
* when using NIO connector. Setting this value to false means that
* a direct buffer will be used instead of a mapped file buffer.
* buffers sized with the above parameters will be used instead of a mapped file buffer.
* This is set to false by default by this class, but may be overridden
* by eg webdefault-ee9.xml
*
@ -255,7 +259,9 @@ public class DefaultServlet extends HttpServlet implements WelcomeFactory
HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName());
if (contentFactory == null)
{
contentFactory = new ResourceHttpContentFactory(_baseResource, _mimeTypes)
ByteBufferPool bufferPool = getByteBufferPool(_contextHandler);
ByteBufferPool.Sized sizedBufferPool = new ByteBufferPool.Sized(bufferPool, getInitBoolean("useDirectByteBuffers", true), getInitInt("byteBufferSize", 32768));
contentFactory = new ResourceHttpContentFactory(_baseResource, _mimeTypes, sizedBufferPool)
{
@Override
protected Resource resolve(String pathInContext)
@ -265,7 +271,7 @@ public class DefaultServlet extends HttpServlet implements WelcomeFactory
};
if (_useFileMappedBuffer)
contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = new VirtualHttpContentFactory(contentFactory, _styleSheet, "text/css");
contentFactory = new VirtualHttpContentFactory(contentFactory, _styleSheet, "text/css", sizedBufferPool);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, _resourceService.getPrecompressedFormats());
int maxCacheSize = getInitInt("maxCacheSize", -2);
@ -274,7 +280,6 @@ public class DefaultServlet extends HttpServlet implements WelcomeFactory
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{
ByteBufferPool bufferPool = getByteBufferPool(_contextHandler);
_cachingContentFactory = new ValidatingCachingHttpContentFactory(contentFactory,
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
contentFactory = _cachingContentFactory;

View File

@ -19,7 +19,6 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
@ -51,7 +50,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.HttpConfiguration;
@ -2763,7 +2762,7 @@ public class DefaultServletTest
{
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
});
@ -2791,7 +2790,7 @@ public class DefaultServletTest
{
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
});
@ -2822,16 +2821,7 @@ public class DefaultServletTest
{
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), null, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
});
@ -2859,16 +2849,7 @@ public class DefaultServletTest
{
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = IOResources.toRetainableByteBuffer(getResource(), null, false).getByteBuffer();
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
});
@ -2899,7 +2880,7 @@ public class DefaultServletTest
{
Resource memResource = ResourceFactory.of(context).newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain", ByteBufferPool.SIZED_NON_POOLING));
resourceService.setAcceptRanges(false);
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");