| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.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.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * <p>A specialized transformer for {@link AsyncMiddleManServlet} that performs |
| * the transformation when the whole content has been received.</p> |
| * <p>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.</p> |
| * <p>Application must implement the {@link #transform(Source, Sink) transformation method} |
| * to transform the content.</p> |
| * <p>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.</p> |
| */ |
| public abstract class AfterContentTransformer implements AsyncMiddleManServlet.ContentTransformer, Destroyable |
| { |
| private static final Logger LOG = Log.getLogger(AfterContentTransformer.class); |
| |
| private final List<ByteBuffer> 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; |
| |
| /** |
| * <p>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}.</p> |
| * <p>Defaults to the directory pointed by the {@code java.io.tmpdir} |
| * system property.</p> |
| * |
| * @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; |
| } |
| |
| /** |
| * <p>Returns the maximum input buffer size, after which the input is overflown to disk.</p> |
| * <p>Defaults to 1 MiB, i.e. 1048576 bytes.</p> |
| * |
| * @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; |
| } |
| |
| /** |
| * <p>Returns the maximum output buffer size, after which the output is overflown to disk.</p> |
| * <p>Defaults to 1 MiB, i.e. 1048576 bytes.</p> |
| * |
| * @return the max output buffer size |
| * @see #setMaxOutputBufferSize(long) |
| */ |
| public long getMaxOutputBufferSize() |
| { |
| return maxOutputBufferSize; |
| } |
| |
| /** |
| * @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<ByteBuffer> 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); |
| } |
| } |
| |
| /** |
| * <p>Transforms the original content read from the {@code source} into |
| * transformed content written to the {@code sink}.</p> |
| * <p>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).</p> |
| * <p>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.</p> |
| * <p>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.</p> |
| * <p>Typical implementations:</p> |
| * <pre> |
| * // 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; |
| * } |
| * </pre> |
| * |
| * @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<ByteBuffer> 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.ignore(x); |
| } |
| } |
| |
| /** |
| * <p>The source from where the original content is read to be transformed.</p> |
| * <p>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.</p> |
| */ |
| 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<ByteBuffer> 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; |
| } |
| } |
| |
| /** |
| * <p>The target to where the transformed content is written after the transformation.</p> |
| */ |
| public class Sink |
| { |
| private final List<ByteBuffer> 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<ByteBuffer> 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); |
| } |
| } |
| } |
| } |