org.eclipse.jetty.ee10.proxy.AfterContentTransformer 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.ee10.proxy;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.Destroyable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A specialized transformer for {@link AsyncMiddleManServlet} that performs
* the transformation when the whole content has been received.
* The content is buffered in memory up to a configurable {@link #getMaxInputBufferSize() maximum size},
* after which it is overflown to a file on disk. The overflow file is saved
* in the {@link #getOverflowDirectory() overflow directory} as a
* {@link Files#createTempFile(Path, String, String, FileAttribute[]) temporary file}
* with a name starting with the {@link #getInputFilePrefix() input prefix}
* and default suffix.
* Application must implement the {@link #transform(Source, Sink) transformation method}
* to transform the content.
* The transformed content is buffered in memory up to a configurable {@link #getMaxOutputBufferSize() maximum size}
* after which it is overflown to a file on disk. The overflow file is saved
* in the {@link #getOverflowDirectory() overflow directory} as a
* {@link Files#createTempFile(Path, String, String, FileAttribute[]) temporary file}
* with a name starting with the {@link #getOutputFilePrefix()} output prefix}
* and default suffix.
*/
public abstract class AfterContentTransformer implements AsyncMiddleManServlet.ContentTransformer, Destroyable
{
private static final Logger LOG = LoggerFactory.getLogger(AfterContentTransformer.class);
private final List sourceBuffers = new ArrayList<>();
private Path overflowDirectory = Paths.get(System.getProperty("java.io.tmpdir"));
private String inputFilePrefix = "amms_adct_in_";
private String outputFilePrefix = "amms_adct_out_";
private long maxInputBufferSize = 1024 * 1024;
private long inputBufferSize;
private FileChannel inputFile;
private long maxOutputBufferSize = maxInputBufferSize;
private long outputBufferSize;
private FileChannel outputFile;
/**
* Returns the directory where input and output are overflown to
* temporary files if they exceed, respectively, the
* {@link #getMaxInputBufferSize() max input size} or the
* {@link #getMaxOutputBufferSize() max output size}.
* Defaults to the directory pointed by the {@code java.io.tmpdir}
* system property.
*
* @return the overflow directory path
* @see #setOverflowDirectory(Path)
*/
public Path getOverflowDirectory()
{
return overflowDirectory;
}
/**
* @param overflowDirectory the overflow directory path
* @see #getOverflowDirectory()
*/
public void setOverflowDirectory(Path overflowDirectory)
{
this.overflowDirectory = overflowDirectory;
}
/**
* @return the prefix of the input overflow temporary files
* @see #setInputFilePrefix(String)
*/
public String getInputFilePrefix()
{
return inputFilePrefix;
}
/**
* @param inputFilePrefix the prefix of the input overflow temporary files
* @see #getInputFilePrefix()
*/
public void setInputFilePrefix(String inputFilePrefix)
{
this.inputFilePrefix = inputFilePrefix;
}
/**
* Returns the maximum input buffer size, after which the input is overflown to disk.
* Defaults to 1 MiB, i.e. 1048576 bytes.
*
* @return the max input buffer size
* @see #setMaxInputBufferSize(long)
*/
public long getMaxInputBufferSize()
{
return maxInputBufferSize;
}
/**
* @param maxInputBufferSize the max input buffer size
* @see #getMaxInputBufferSize()
*/
public void setMaxInputBufferSize(long maxInputBufferSize)
{
this.maxInputBufferSize = maxInputBufferSize;
}
/**
* @return the prefix of the output overflow temporary files
* @see #setOutputFilePrefix(String)
*/
public String getOutputFilePrefix()
{
return outputFilePrefix;
}
/**
* @param outputFilePrefix the prefix of the output overflow temporary files
* @see #getOutputFilePrefix()
*/
public void setOutputFilePrefix(String outputFilePrefix)
{
this.outputFilePrefix = outputFilePrefix;
}
/**
* Returns the maximum output buffer size, after which the output is overflown to disk.
* Defaults to 1 MiB, i.e. 1048576 bytes.
*
* @return the max output buffer size
* @see #setMaxOutputBufferSize(long)
*/
public long getMaxOutputBufferSize()
{
return maxOutputBufferSize;
}
/**
* Set the max output buffer size.
* @param maxOutputBufferSize the max output buffer size
*/
public void setMaxOutputBufferSize(long maxOutputBufferSize)
{
this.maxOutputBufferSize = maxOutputBufferSize;
}
@Override
public final void transform(ByteBuffer input, boolean finished, List output) throws IOException
{
int remaining = input.remaining();
if (remaining > 0)
{
inputBufferSize += remaining;
long max = getMaxInputBufferSize();
if (max >= 0 && inputBufferSize > max)
{
overflow(input);
}
else
{
ByteBuffer copy = ByteBuffer.allocate(input.remaining());
copy.put(input).flip();
sourceBuffers.add(copy);
}
}
if (finished)
{
Source source = new Source();
Sink sink = new Sink();
if (transform(source, sink))
sink.drainTo(output);
else
source.drainTo(output);
}
}
/**
* Transforms the original content read from the {@code source} into
* transformed content written to the {@code sink}.
* The transformation must happen synchronously in the context of a call
* to this method (it is not supported to perform the transformation in another
* thread spawned during the call to this method).
* Differently from {@link #transform(ByteBuffer, boolean, List)}, this
* method is invoked only when the whole content is available, and offers
* a blocking API via the InputStream and OutputStream that can be obtained
* from {@link Source} and {@link Sink} respectively.
* Implementations may read the source, inspect the input bytes and decide
* that no transformation is necessary, and therefore the source must be copied
* unchanged to the sink. In such case, the implementation must return false to
* indicate that it wishes to just pipe the bytes from the source to the sink.
* Typical implementations:
*
* // Identity transformation (no transformation, the input is copied to the output)
* public boolean transform(Source source, Sink sink)
* {
* org.eclipse.jetty.util.IO.copy(source.getInputStream(), sink.getOutputStream());
* return true;
* }
*
*
* @param source where the original content is read
* @param sink where the transformed content is written
* @return true if the transformation happened and the transformed bytes have
* been written to the sink, false if no transformation happened and the source
* must be copied to the sink.
* @throws IOException if the transformation fails
*/
public abstract boolean transform(Source source, Sink sink) throws IOException;
private void overflow(ByteBuffer input) throws IOException
{
if (inputFile == null)
{
Path path = Files.createTempFile(getOverflowDirectory(), getInputFilePrefix(), null);
inputFile = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.DELETE_ON_CLOSE);
int size = sourceBuffers.size();
if (size > 0)
{
ByteBuffer[] buffers = sourceBuffers.toArray(new ByteBuffer[size]);
sourceBuffers.clear();
IO.write(inputFile, buffers, 0, buffers.length);
}
}
inputFile.write(input);
}
@Override
public void destroy()
{
close(inputFile);
close(outputFile);
}
private void drain(FileChannel file, List output) throws IOException
{
long position = 0;
long length = file.size();
file.position(position);
while (length > 0)
{
// At most 1 GiB file maps.
long size = Math.min(1024 * 1024 * 1024, length);
ByteBuffer buffer = file.map(FileChannel.MapMode.READ_ONLY, position, size);
output.add(buffer);
position += size;
length -= size;
}
}
private void close(Closeable closeable)
{
try
{
if (closeable != null)
closeable.close();
}
catch (IOException x)
{
LOG.trace("IGNORED", x);
}
}
/**
* The source from where the original content is read to be transformed.
* The {@link #getInputStream() input stream} provided by this
* class supports the {@link InputStream#reset()} method so that
* the stream can be rewound to the beginning.
*/
public class Source
{
private final InputStream stream;
private Source() throws IOException
{
if (inputFile != null)
{
inputFile.force(true);
stream = new ChannelInputStream();
}
else
{
stream = new MemoryInputStream();
}
stream.reset();
}
/**
* @return an input stream to read the original content from
*/
public InputStream getInputStream()
{
return stream;
}
private void drainTo(List output) throws IOException
{
if (inputFile == null)
{
output.addAll(sourceBuffers);
sourceBuffers.clear();
}
else
{
drain(inputFile, output);
}
}
}
private class ChannelInputStream extends InputStream
{
private final InputStream stream = Channels.newInputStream(inputFile);
@Override
public int read(byte[] b, int off, int len) throws IOException
{
return stream.read(b, off, len);
}
@Override
public int read() throws IOException
{
return stream.read();
}
@Override
public void reset() throws IOException
{
inputFile.position(0);
}
}
private class MemoryInputStream extends InputStream
{
private final byte[] oneByte = new byte[1];
private int index;
private ByteBuffer slice;
@Override
public int read(byte[] b, int off, int len) throws IOException
{
if (len == 0)
return 0;
if (index == sourceBuffers.size())
return -1;
if (slice == null)
slice = sourceBuffers.get(index).slice();
int size = Math.min(len, slice.remaining());
slice.get(b, off, size);
if (!slice.hasRemaining())
{
++index;
slice = null;
}
return size;
}
@Override
public int read() throws IOException
{
int read = read(oneByte, 0, 1);
return read < 0 ? read : oneByte[0] & 0xFF;
}
@Override
public void reset() throws IOException
{
index = 0;
slice = null;
}
}
/**
* The target to where the transformed content is written after the transformation.
*/
public class Sink
{
private final List sinkBuffers = new ArrayList<>();
private final OutputStream stream = new SinkOutputStream();
/**
* @return an output stream to write the transformed content to
*/
public OutputStream getOutputStream()
{
return stream;
}
private void overflow(ByteBuffer output) throws IOException
{
if (outputFile == null)
{
Path path = Files.createTempFile(getOverflowDirectory(), getOutputFilePrefix(), null);
outputFile = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.DELETE_ON_CLOSE);
int size = sinkBuffers.size();
if (size > 0)
{
ByteBuffer[] buffers = sinkBuffers.toArray(new ByteBuffer[size]);
sinkBuffers.clear();
IO.write(outputFile, buffers, 0, buffers.length);
}
}
outputFile.write(output);
}
private void drainTo(List output) throws IOException
{
if (outputFile == null)
{
output.addAll(sinkBuffers);
sinkBuffers.clear();
}
else
{
outputFile.force(true);
drain(outputFile, output);
}
}
private class SinkOutputStream extends OutputStream
{
@Override
public void write(byte[] b, int off, int len) throws IOException
{
if (len <= 0)
return;
outputBufferSize += len;
long max = getMaxOutputBufferSize();
if (max >= 0 && outputBufferSize > max)
{
overflow(ByteBuffer.wrap(b, off, len));
}
else
{
// The array may be reused by the
// application so we need to copy it.
byte[] copy = new byte[len];
System.arraycopy(b, off, copy, 0, len);
sinkBuffers.add(ByteBuffer.wrap(copy));
}
}
@Override
public void write(int b) throws IOException
{
write(new byte[]{(byte)b}, 0, 1);
}
}
}
}