org.eclipse.jetty.io.Content Maven / Gradle / Ivy
//
// ========================================================================
// 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.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
import java.util.function.Consumer;
import org.eclipse.jetty.io.content.BufferedContentSink;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.io.content.ContentSinkOutputStream;
import org.eclipse.jetty.io.content.ContentSinkSubscriber;
import org.eclipse.jetty.io.content.ContentSourceInputStream;
import org.eclipse.jetty.io.content.ContentSourcePublisher;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.io.internal.ByteBufferChunk;
import org.eclipse.jetty.io.internal.ByteChannelContentSource;
import org.eclipse.jetty.io.internal.ContentCopier;
import org.eclipse.jetty.io.internal.ContentSourceByteBuffer;
import org.eclipse.jetty.io.internal.ContentSourceConsumer;
import org.eclipse.jetty.io.internal.ContentSourceString;
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.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Namespace class that contains the definitions of a {@link Source content source},
* a {@link Sink content sink} and a {@link Chunk content chunk}.
*/
public class Content
{
private static final Logger LOG = LoggerFactory.getLogger(Content.class);
private Content()
{
}
/**
* Copies the given content source to the given content sink, notifying
* the given callback when the copy is complete (either succeeded or failed).
* In case of {@link Chunk#getFailure() failure chunks},
* the content source is {@link Source#fail(Throwable) failed}.
*
* @param source the source to copy from
* @param sink the sink to copy to
* @param callback the callback to notify when the copy is complete
* @see #copy(Source, Sink, Chunk.Processor, Callback) to allow processing of individual {@link Chunk}s, including
* the ability to ignore transient failures.
*/
public static void copy(Source source, Sink sink, Callback callback)
{
copy(source, sink, null, callback);
}
/**
* Copies the given content source to the given content sink, notifying
* the given callback when the copy is complete.
* The optional {@code chunkHandler} parameter is a predicate whose code
* may inspect the chunk and handle it differently from how the implementation
* would handle it.
* If the predicate returns {@code true}, it means that the chunk is handled
* externally and its callback completed, or eventually completed.
* If the predicate returns {@code false}, it means that the chunk is not
* handled, its callback will not be completed, and the implementation will
* handle the chunk and its callback.
* In case of {@link Chunk#getFailure() failure chunks} not handled by any {@code chunkHandler},
* the content source is {@link Source#fail(Throwable) failed} if the failure
* chunk is {@link Chunk#isLast() last}, else the failure is transient and is ignored.
*
* @param source the source to copy from
* @param sink the sink to copy to
* @param chunkProcessor a (possibly {@code null}) processor to handle the current {@link Chunk} and its callback
* @param callback the callback to notify when the copy is complete
*/
public static void copy(Source source, Sink sink, Chunk.Processor chunkProcessor, Callback callback)
{
new ContentCopier(source, sink, chunkProcessor, callback).iterate();
}
/**
* A source of content that can be read with a read/demand model.
* To avoid leaking its resources, a source must either:
*
* - be read until it returns a {@link Chunk#isLast() last chunk}, either EOF or a terminal failure
* - be {@link #fail(Throwable) failed}
*
* Idiomatic usage
* The read/demand model typical usage is the following:
* {@code
* public void onContentAvailable() {
* while (true) {
* // Read a chunk
* Chunk chunk = source.read();
*
* // There is no chunk, demand to be called back and exit.
* if (chunk == null) {
* source.demand(this::onContentAvailable);
* return;
* }
*
* // The chunk is a failure.
* if (Content.Chunk.isFailure(chunk))
* {
* boolean fatal = chunk.isLast();
* if (fatal)
* {
* handleFatalFailure(chunk.getFailure());
* return;
* }
* else
* {
* handleTransientFailure(chunk.getFailure());
* continue;
* }
* }
*
* // It's a valid chunk, consume the chunk's bytes.
* ByteBuffer buffer = chunk.getByteBuffer();
* // ...
*
* // Release the chunk when it has been consumed.
* chunk.release();
*
* // Exit if the Content.Source is fully consumed.
* if (chunk.isLast())
* break;
* }
* }
* }
*/
public interface Source
{
/**
* Create a {@code Content.Source} from zero or more {@link ByteBuffer}s
* @param byteBuffers The {@link ByteBuffer}s to use as the source.
* @return A {@code Content.Source}
*/
static Content.Source from(ByteBuffer... byteBuffers)
{
return new ByteBufferContentSource(byteBuffers);
}
/**
* Create a {@code Content.Source} from a {@link Path}.
* @param path The {@link Path}s to use as the source.
* @return A {@code Content.Source}
*/
static Content.Source from(Path path)
{
return from(null, path, 0, -1);
}
/**
* Create a {@code Content.Source} from a {@link Path}.
* @param path The {@link Path}s to use as the source.
* @param offset The offset in bytes from which to start the source
* @param length The length in bytes of the source.
* @return A {@code Content.Source}
*/
static Content.Source from(Path path, long offset, long length)
{
return from(null, path, offset, length);
}
/**
* Create a {@code Content.Source} from a {@link Path}.
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
* @param path The {@link Path}s to use as the source.
* @return A {@code Content.Source}
*/
static Content.Source from(ByteBufferPool.Sized byteBufferPool, Path path)
{
return from(byteBufferPool, path, 0, -1);
}
/**
* Create a {@code Content.Source} from a {@link Path}.
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
* @param path The {@link Path}s to use as the source.
* @param offset The offset in bytes from which to start the source
* @param length The length in bytes of the source.
* @return A {@code Content.Source}
*/
static Content.Source from(ByteBufferPool.Sized byteBufferPool, Path path, long offset, long length)
{
return new ByteChannelContentSource.PathContentSource(byteBufferPool, path, offset, length);
}
/**
* Create a {@code Content.Source} from a {@link ByteChannel}.
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
* @param byteChannel The {@link ByteChannel}s to use as the source.
* @return A {@code Content.Source}
*/
static Content.Source from(ByteBufferPool.Sized byteBufferPool, ByteChannel byteChannel)
{
return new ByteChannelContentSource(byteBufferPool, byteChannel);
}
/**
* Create a {@code Content.Source} from a {@link ByteChannel}.
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
* @param seekableByteChannel The {@link ByteChannel}s to use as the source.
* @param offset The offset in bytes from which to start the source
* @param length The length in bytes of the source.
* @return A {@code Content.Source}
*/
static Content.Source from(ByteBufferPool.Sized byteBufferPool, SeekableByteChannel seekableByteChannel, long offset, long length)
{
return new ByteChannelContentSource(byteBufferPool, seekableByteChannel, offset, length);
}
static Content.Source from(InputStream inputStream)
{
return from(null, inputStream);
}
/**
* Create a {@code Content.Source} from a {@link Path}.
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
* @param inputStream The {@link InputStream}s to use as the source.
* @return A {@code Content.Source}
*/
static Content.Source from(ByteBufferPool.Sized byteBufferPool, InputStream inputStream)
{
return new InputStreamContentSource(inputStream, byteBufferPool);
}
/**
* Create a {@code Content.Source} from a {@link Path}.
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
* @param inputStream The {@link InputStream}s to use as the source.
* @param offset The offset in bytes from which to start the source
* @param length The length in bytes of the source.
* @return A {@code Content.Source}
*/
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;
}
};
}
/**
* Reads, non-blocking, the whole content source into a {@link ByteBuffer}.
*
* @param source the source to read
* @param promise the promise to notify when the whole content has been read into a ByteBuffer.
*/
static void asByteBuffer(Source source, Promise promise)
{
new ContentSourceByteBuffer(source, promise).run();
}
/**
* Reads, blocking if necessary, the whole content source into a {@link ByteBuffer}.
*
* @param source the source to read
* @return the ByteBuffer containing the content
* @throws IOException if reading the content fails
*/
static ByteBuffer asByteBuffer(Source source) throws IOException
{
try
{
try (Blocker.Promise promise = Blocker.promise())
{
asByteBuffer(source, promise);
return promise.block();
}
}
catch (Throwable x)
{
throw IO.rethrow(x);
}
}
/**
* Reads, non-blocking, the whole content source into a {@code byte} array.
*
* @param source the source to read
* @param maxSize The maximum size to read, or -1 for no limit
* @return A {@link CompletableFuture} that will be completed when the complete content is read or
* failed if the max size is exceeded or there is a read error.
* @deprecated no replacement
*/
@Deprecated(forRemoval = true, since = "12.0.15")
static CompletableFuture asByteArrayAsync(Source source, int maxSize)
{
return new ChunkAccumulator().readAll(source, maxSize);
}
/**
* Reads, non-blocking, the whole content source into a {@link ByteBuffer}.
*
* @param source the source to read
* @return the {@link CompletableFuture} to notify when the whole content has been read
* @deprecated use {@link #asByteBuffer(Source, Promise)} instead
*/
@Deprecated(forRemoval = true, since = "12.0.15")
static CompletableFuture asByteBufferAsync(Source source)
{
return asByteBufferAsync(source, -1);
}
/**
* Reads, non-blocking, the whole content source into a {@link ByteBuffer}.
*
* @param source the source to read
* @param maxSize The maximum size to read, or -1 for no limit
* @return the {@link CompletableFuture} to notify when the whole content has been read
* @deprecated no replacement
*/
@Deprecated(forRemoval = true, since = "12.0.15")
static CompletableFuture asByteBufferAsync(Source source, int maxSize)
{
return asByteArrayAsync(source, maxSize).thenApply(ByteBuffer::wrap);
}
/**
* Reads, non-blocking, the whole content source into a {@link RetainableByteBuffer}.
*
* @param source The {@link Content.Source} to read
* @param pool The {@link ByteBufferPool} to acquire the buffer from, or null for a non {@link Retainable} buffer
* @param direct True if the buffer should be direct.
* @param maxSize The maximum size to read, or -1 for no limit
* @return A {@link CompletableFuture} that will be completed when the complete content is read or
* failed if the max size is exceeded or there is a read error.
* @deprecated no replacement
*/
@Deprecated(forRemoval = true, since = "12.0.15")
static CompletableFuture asRetainableByteBuffer(Source source, ByteBufferPool pool, boolean direct, int maxSize)
{
return new ChunkAccumulator().readAll(source, pool, direct, maxSize);
}
/**
* Reads, non-blocking, the whole content source into a {@link String}, converting the bytes
* using the given {@link Charset}.
*
* @param source the source to read
* @param charset the charset to use to convert the bytes into characters
* @param promise the promise to notify when the whole content has been converted into a String
*/
static void asString(Source source, Charset charset, Promise promise)
{
new ContentSourceString(source, charset, promise).convert();
}
/**
* Reads, blocking if necessary, the whole content source into a {@link String}, converting
* the bytes using UTF-8.
*
* @param source the source to read
* @return the String obtained from the content
* @throws IOException if reading the content fails
*/
static String asString(Source source) throws IOException
{
return asString(source, StandardCharsets.UTF_8);
}
/**
* Reads, blocking if necessary, the whole content source into a {@link String}, converting
* the bytes using the given {@link Charset}.
*
* @param source the source to read
* @param charset the charset to use to decode bytes
* @return the String obtained from the content
* @throws IOException if reading the content fails
*/
static String asString(Source source, Charset charset) throws IOException
{
try
{
try (Blocker.Promise promise = Blocker.promise())
{
asString(source, charset, promise);
return promise.block();
}
}
catch (Throwable x)
{
throw IO.rethrow(x);
}
}
/**
* Read, non-blocking, the whole content source into a {@link String}, converting
* the bytes using the given {@link Charset}.
*
* @param source the source to read
* @param charset the charset to use to decode bytes
* @return the {@link CompletableFuture} to notify when the whole content has been read
* @deprecated use {@link #asString(Source, Charset, Promise)} instead
*/
@Deprecated(forRemoval = true, since = "12.0.15")
static CompletableFuture asStringAsync(Source source, Charset charset)
{
Promise.Completable completable = new Promise.Completable<>();
asString(source, charset, completable);
return completable;
}
/**
* Wraps the given content source with an {@link InputStream}.
*
* @param source the source to read from
* @return an InputStream that reads from the content source
*/
static InputStream asInputStream(Source source)
{
return new ContentSourceInputStream(source);
}
/**
* Wraps the given content source with a {@link Flow.Publisher}.
*
* @param source the source to read from
* @return a Publisher that publishes chunks read from the content source
*/
static Flow.Publisher asPublisher(Source source)
{
return new ContentSourcePublisher(source);
}
/**
* Reads, non-blocking, the given content source, until a {@link Chunk#isFailure(Chunk) failure} or EOF
* and discards the content.
*
* @param source the source to read from
* @param callback the callback to notify when the whole content has been read
* or a failure occurred while reading the content
*/
static void consumeAll(Source source, Callback callback)
{
new ContentSourceConsumer(source, callback).run();
}
/**
* Reads, blocking if necessary, the given content source, until a {@link Chunk#isFailure(Chunk) failure}
* or EOF, and discards the content.
*
* @param source the source to read from
* @throws IOException if reading the content fails
*/
static void consumeAll(Source source) throws IOException
{
try
{
try (Blocker.Callback callback = Blocker.callback())
{
consumeAll(source, callback);
callback.block();
}
}
catch (Throwable x)
{
throw IO.rethrow(x);
}
}
/**
* @return the content length, if known, or -1 if the content length is unknown
*/
default long getLength()
{
return -1;
}
/**
* Reads a chunk of content.
* See how to use this method idiomatically.
* The returned chunk could be:
*
* - {@code null}, to signal that there isn't a chunk of content available
* - an {@link Chunk} instance with non null {@link Chunk#getFailure()}, to signal that there was a failure
* trying to produce a chunk of content, or that the content production has been
* {@link #fail(Throwable) failed} externally
* - a {@link Chunk} instance, containing the chunk of content.
*
* Once a read returns an {@link Chunk} instance with non-null {@link Chunk#getFailure()}
* then if the failure is {@link Chunk#isLast() last} further reads
* will continue to return the same failure chunk instance, otherwise further
* {@code read()} operations may return different non-failure chunks.
* Once a read returns a {@link Chunk#isLast() last chunk}, further reads will
* continue to return a last chunk (although the instance may be different).
* The content reader code must ultimately arrange for a call to
* {@link Chunk#release()} on the returned {@link Chunk}.
* Additionally, prior to the ultimate call to {@link Chunk#release()}, the reader
* code may make additional calls to {@link Chunk#retain()}, that must ultimately
* be matched by a correspondent number of calls to {@link Chunk#release()}.
* Concurrent reads from different threads are not recommended, as they are
* inherently in a race condition.
* Reads performed outside the invocation context of a
* {@link #demand(Runnable) demand callback} are allowed.
* However, reads performed with a pending demand are inherently in a
* race condition (the thread that reads with the thread that invokes the
* demand callback).
*
* @return a chunk of content, possibly a failure instance, or {@code null}
* @see #demand(Runnable)
* @see Retainable
*/
Chunk read();
/**
* Demands to invoke the given demand callback parameter when a chunk of content is available.
* See how to use this method idiomatically.
* Implementations guarantee that calls to this method are safely reentrant so that
* stack overflows are avoided in the case of mutual recursion between the execution of
* the {@code Runnable} callback and a call to this method. Invocations of the passed
* {@code Runnable} are serialized and a callback for {@code demand} call is
* not invoked until any previous {@code demand} callback has returned.
* Thus the {@code Runnable} should not block waiting for a callback of a future demand call.
* The demand callback may be invoked spuriously: a subsequent call to {@link #read()}
* may return {@code null}.
* Calling this method establishes a pending demand, which is fulfilled when the demand
* callback is invoked.
* Calling this method when there is already a pending demand results in an
* {@link IllegalStateException} to be thrown.
* If the invocation of the demand callback throws an exception, then {@link #fail(Throwable)}
* is called.
*
* @param demandCallback the demand callback to invoke where there is a content chunk available
* @throws IllegalStateException when this method is called with an existing demand
* @see #read()
*/
void demand(Runnable demandCallback);
/**
* Fails this content source with a {@link Chunk#isLast() last} {@link Chunk#getFailure() failure chunk},
* failing and discarding accumulated content chunks that were not yet read.
* The failure may be notified to the content reader at a later time, when
* the content reader reads a content chunk, via a {@link Chunk} instance
* with a non null {@link Chunk#getFailure()}.
* If {@link #read()} has returned a last chunk, this is a no operation.
* Typical failure: the content being aborted by user code, or idle timeouts.
* If this method has already been called, then it is a no operation.
*
* @param failure the cause of the failure
* @see Chunk#getFailure()
*/
void fail(Throwable failure);
/**
* Fails this content source with a {@link Chunk#getFailure() failure chunk}
* that may or not may be {@link Chunk#isLast() last}.
* If {@code last} is {@code true}, then the failure is persistent and a call to this method acts
* as {@link #fail(Throwable)}. Otherwise the failure is transient and a
* {@link Chunk#getFailure() failure chunk} will be {@link #read() read} in order with content chunks,
* and subsequent calls to {@link #read() read} may produce other content.
* A {@code Content.Source} or its {@link #read() reader} may treat a transient failure as persistent.
*
* @param failure A failure.
* @param last true if the failure is persistent, false if the failure is transient.
* @see Chunk#getFailure()
*/
default void fail(Throwable failure, boolean last)
{
fail(failure);
}
/**
* Rewinds this content, if possible, so that subsequent reads return
* chunks starting from the beginning of this content.
*
* @return true if this content has been rewound, false if this content
* cannot be rewound
*/
default boolean rewind()
{
return false;
}
}
/**
* A content sink that writes the content to its implementation (a socket, a file, etc.).
*/
public interface Sink
{
/**
* Wraps the given content sink with a buffering sink.
*
* @param sink the sink to write to
* @param bufferPool the {@link ByteBufferPool} to use
* @param direct true to use direct buffers, false to use heap buffers
* @param maxAggregationSize the maximum size that can be buffered in a single write;
* any size above this threshold triggers a buffer flush
* @param maxBufferSize the maximum size of the buffer
* @return a Sink that writes to the given content sink
*/
static Sink asBuffered(Sink sink, ByteBufferPool bufferPool, boolean direct, int maxAggregationSize, int maxBufferSize)
{
return new BufferedContentSink(sink, bufferPool, direct, maxAggregationSize, maxBufferSize);
}
/**
* Wraps the given content sink with an {@link OutputStream}.
*
* @param sink the sink to write to
* @return an OutputStream that writes to the content sink
*/
static OutputStream asOutputStream(Sink sink)
{
return new ContentSinkOutputStream(sink);
}
/**
* Wraps the given content sink with a {@link Flow.Subscriber}.
*
* @param sink the sink to write to
* @param callback the callback to notify when the Subscriber is complete
* @return a Subscriber that writes to the content sink
*/
static Flow.Subscriber asSubscriber(Sink sink, Callback callback)
{
return new ContentSinkSubscriber(sink, callback);
}
/**
* Blocking version of {@link #write(boolean, ByteBuffer, Callback)}.
*
* @param sink the sink to write to
* @param last whether the ByteBuffers are the last to write
* @param byteBuffer the ByteBuffers to write
* @throws IOException if the write operation fails
*/
static void write(Sink sink, boolean last, ByteBuffer byteBuffer) throws IOException
{
try (Blocker.Callback callback = Blocker.callback())
{
sink.write(last, byteBuffer, callback);
callback.block();
}
}
/**
* Writes the given {@link String}, converting it to UTF-8 bytes,
* notifying the {@link Callback} when the write is complete.
*
* @param last whether the String is the last to write
* @param utf8Content the String to write
* @param callback the callback to notify when the write operation is complete.
* Implementations have the same guarantees for invocation of this
* callback as for {@link #write(boolean, ByteBuffer, Callback)}.
*/
static void write(Sink sink, boolean last, String utf8Content, Callback callback)
{
sink.write(last, StandardCharsets.UTF_8.encode(utf8Content), callback);
}
/**
* Writes the given {@link ByteBuffer}, notifying the {@link Callback}
* when the write is complete.
* Implementations guarantee that calls to this method are safely reentrant so that
* stack overflows are avoided in the case of mutual recursion between the execution of
* the {@code Callback} and a call to this method.
*
* @param last whether the ByteBuffer is the last to write
* @param byteBuffer the ByteBuffer to write
* @param callback the callback to notify when the write operation is complete
*/
void write(boolean last, ByteBuffer byteBuffer, Callback callback);
}
/**
* A chunk of content indicating whether it is the last chunk.
* Optionally, a release function may be specified (for example
* to release the {@code ByteBuffer} back into a pool), or the
* {@link #release()} method overridden.
*/
public interface Chunk extends Retainable
{
/**
* An empty, non-last, chunk.
*/
Chunk EMPTY = new Chunk()
{
@Override
public ByteBuffer getByteBuffer()
{
return BufferUtil.EMPTY_BUFFER;
}
@Override
public boolean isLast()
{
return false;
}
@Override
public String toString()
{
return "EMPTY";
}
};
/**
* An empty, last, chunk.
*/
Content.Chunk EOF = new Chunk()
{
@Override
public ByteBuffer getByteBuffer()
{
return BufferUtil.EMPTY_BUFFER;
}
@Override
public boolean isLast()
{
return true;
}
@Override
public String toString()
{
return "EOF";
}
};
/**
* Creates a Chunk with the given ByteBuffer.
* The returned Chunk must be {@link #release() released}.
*
* @param byteBuffer the ByteBuffer with the bytes of this Chunk
* @param last whether the Chunk is the last one
* @return a new Chunk
*/
static Chunk from(ByteBuffer byteBuffer, boolean last)
{
if (byteBuffer.hasRemaining())
return new ByteBufferChunk.WithReferenceCount(byteBuffer, last);
return last ? EOF : EMPTY;
}
/**
* Creates a Chunk with the given ByteBuffer.
* The returned Chunk must be {@link #release() released}.
*
* @param byteBuffer the ByteBuffer with the bytes of this Chunk
* @param last whether the Chunk is the last one
* @param releaser the code to run when this Chunk is released
* @return a new Chunk
*/
static Chunk from(ByteBuffer byteBuffer, boolean last, Runnable releaser)
{
if (byteBuffer.hasRemaining())
return new ByteBufferChunk.ReleasedByRunnable(byteBuffer, last, Objects.requireNonNull(releaser));
releaser.run();
return last ? EOF : EMPTY;
}
/**
* Creates a last/non-last Chunk with the given ByteBuffer.
* The returned Chunk must be {@link #release() released}.
*
* @param byteBuffer the ByteBuffer with the bytes of this Chunk
* @param last whether the Chunk is the last one
* @param releaser the code to run when this Chunk is released
* @return a new Chunk
*/
static Chunk from(ByteBuffer byteBuffer, boolean last, Consumer releaser)
{
if (byteBuffer.hasRemaining())
return new ByteBufferChunk.ReleasedByConsumer(byteBuffer, last, Objects.requireNonNull(releaser));
releaser.accept(byteBuffer);
return last ? EOF : EMPTY;
}
/**
* Returns the given {@code ByteBuffer} and {@code last} arguments
* as a {@code Chunk}, linked to the given {@link Retainable}.
* The {@link #retain()} and {@link #release()} methods of this
* {@code Chunk} will delegate to the given {@code Retainable}.
*
* @param byteBuffer the ByteBuffer with the bytes of this Chunk
* @param last whether the Chunk is the last one
* @param retainable the Retainable this Chunk links to
* @return a new Chunk
*/
static Chunk asChunk(ByteBuffer byteBuffer, boolean last, Retainable retainable)
{
if (byteBuffer.hasRemaining())
{
if (retainable.canRetain())
{
return new ByteBufferChunk.WithRetainable(byteBuffer, last, Objects.requireNonNull(retainable));
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Copying buffer because could not retain");
return new ByteBufferChunk.WithReferenceCount(BufferUtil.copy(byteBuffer), last);
}
}
retainable.release();
return last ? EOF : EMPTY;
}
/**
* Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure
* and {@link Chunk#isLast()} returning true.
*
* @param failure the cause of the failure
* @return a new {@link Chunk#isFailure(Chunk) failure chunk}
*/
static Chunk from(Throwable failure)
{
return from(failure, true);
}
/**
* Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure
* and given {@link Chunk#isLast() last} state.
*
* @param failure the cause of the failure
* @param last true if the failure is terminal, else false for transient failure
* @return a new {@link Chunk#isFailure(Chunk) failure chunk}
*/
static Chunk from(Throwable failure, boolean last)
{
return new Chunk()
{
public Throwable getFailure()
{
return failure;
}
@Override
public ByteBuffer getByteBuffer()
{
return BufferUtil.EMPTY_BUFFER;
}
@Override
public boolean isLast()
{
return last;
}
@Override
public String toString()
{
return String.format("Chunk@%x{c=%s,l=%b}", hashCode(), failure, last);
}
};
}
/**
* Returns the chunk that follows the given chunk.
*
* Next Chunk
*
*
* Input Chunk
* Output Chunk
*
*
*
*
* {@code null}
* {@code null}
*
*
* {@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() last}
* {@link Error Error}
*
*
* {@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() not last}
* {@code null}
*
*
* {@link #isLast()}
* {@link #EOF}
*
*
* any other Chunk
* {@code null}
*
*
*
*/
static Chunk next(Chunk chunk)
{
if (chunk == null)
return null;
if (Content.Chunk.isFailure(chunk))
return chunk.isLast() ? chunk : null;
if (chunk.isLast())
return EOF;
return null;
}
/**
* @param chunk The chunk to test for an {@link Chunk#getFailure() failure}.
* @return True if the chunk is non-null and {@link Chunk#getFailure() chunk.getError()} returns non-null.
*/
static boolean isFailure(Chunk chunk)
{
return chunk != null && chunk.getFailure() != null;
}
/**
* @param chunk The chunk to test for an {@link Chunk#getFailure() failure}
* @param last The {@link Chunk#isLast() last} status to test for.
* @return True if the chunk is non-null and {@link Chunk#getFailure()} returns non-null
* and {@link Chunk#isLast()} matches the passed status.
*/
static boolean isFailure(Chunk chunk, boolean last)
{
return chunk != null && chunk.getFailure() != null && chunk.isLast() == last;
}
/**
* @return the ByteBuffer of this Chunk
*/
ByteBuffer getByteBuffer();
/**
* Get a failure (which may be from a {@link Source#fail(Throwable) failure} or
* a {@link Source#fail(Throwable, boolean) warning}), if any, associated with the chunk.
*
* - A {@code chunk} must not have a failure and a {@link #getByteBuffer()} with content.
* - A {@code chunk} with a failure may or may not be {@link #isLast() last}.
* - A {@code chunk} with a failure must not be {@link #canRetain() retainable}.
*
* @return A {@link Throwable} indicating the failure or null if there is no failure or warning.
* @see Source#fail(Throwable)
* @see Source#fail(Throwable, boolean)
*/
default Throwable getFailure()
{
return null;
}
/**
* @return whether this is the last Chunk
*/
boolean isLast();
/**
* @return the number of bytes remaining in this Chunk
*/
default int remaining()
{
return getByteBuffer().remaining();
}
/**
* @return whether this Chunk has remaining bytes
*/
default boolean hasRemaining()
{
return getByteBuffer().hasRemaining();
}
/**
* Copies the bytes from this Chunk to the given byte array.
*
* @param bytes the byte array to copy the bytes into
* @param offset the offset within the byte array
* @param length the maximum number of bytes to copy
* @return the number of bytes actually copied
*/
default int get(byte[] bytes, int offset, int length)
{
ByteBuffer b = getByteBuffer();
if (b == null || !b.hasRemaining())
return 0;
length = Math.min(length, b.remaining());
b.get(bytes, offset, length);
return length;
}
/**
* Skips, advancing the ByteBuffer position, the given number of bytes.
*
* @param length the maximum number of bytes to skip
* @return the number of bytes actually skipped
*/
default int skip(int length)
{
if (length == 0)
return 0;
ByteBuffer byteBuffer = getByteBuffer();
length = Math.min(byteBuffer.remaining(), length);
byteBuffer.position(byteBuffer.position() + length);
return length;
}
/**
* @return an immutable version of this Chunk
*/
default Chunk asReadOnly()
{
if (!getByteBuffer().hasRemaining() || getByteBuffer().isReadOnly())
return this;
if (canRetain())
return asChunk(getByteBuffer().asReadOnlyBuffer(), isLast(), this);
return from(getByteBuffer().asReadOnlyBuffer(), isLast());
}
/**
* Implementations of this interface may process {@link Chunk}s being copied by the
* {@link Content#copy(Source, Sink, Processor, Callback)} method, so that
* {@link Chunk}s of unknown types can be copied.
* @see Content#copy(Source, Sink, Processor, Callback)
*/
interface Processor
{
/**
* @param chunk The chunk to be considered for processing, including persistent and transient failures.
* @param callback The callback that will be called once the accepted chunk is processed.
* {@link Callback#succeeded() Succeeding} this callback will allow the processing of subsequent chunks.
* {@link Callback#failed(Throwable) Failing} this callback will fail the processing of all chunks.
* @return {@code true} if the chunk will be processed asynchronously and the callback will be called (or may have already been called),
* {@code false} otherwise, in which case subsequent chunks may be processed and the passed callback ignored.
*/
boolean process(Chunk chunk, Callback callback);
}
}
}