/* Copyright (c) 2007 Health Market Science, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA You can contact Health Market Science at info@healthmarketscience.com or at the following address: Health Market Science 2700 Horizon Drive Suite 200 King of Prussia, PA 19406 */ package com.healthmarketscience.rmiio.util; import java.io.IOException; import java.nio.BufferUnderflowException; import java.util.LinkedList; import com.healthmarketscience.rmiio.PacketInputStream; import com.healthmarketscience.rmiio.PacketOutputStream; /** * Utility class for implementing a pipe which will never block (at the * expense of possibly utilizing more memory). This is useful for single * threaded pipe implementations (the java.io pipe implementations require * separate threads for reading and writing due to the possibility of the * writer or reader blocking). Additionally, packet based read/write support * is implemented, which can be a significant speed advantage when working in * situations were buffers can be passed around instead of copied (e.g. RMI * usage). In general, this implementation favors speed over memory usage, so * buffers will not be copied if at all possible, even if they are not making * the most efficient usage of memory. This class has no synchronization as * it is designed for use by a single thread. * * @author James Ahlborn */ public class PipeBuffer { /** default target size of packets returned from packet related methods and internal buffer allocation */ public static final int DEFAULT_PACKET_SIZE = 1024; /** the target size of packets returned from packet related methods and internal buffer allocation */ private final int _packetSize; /** the total number of written bytes currently held by this object */ private long _totalBytes; /** the current List of ByteWrappers */ private final LinkedList<ByteWrapper> _buffers = new LinkedList<ByteWrapper>(); /** convenience flag for communicating reader close */ private boolean _gotReadEOF; /** convenience flag for communicating writer close */ private boolean _gotWriteEOF; public PipeBuffer() { this(DEFAULT_PACKET_SIZE); } /** * @param packetSize "suggested" size for packets returned from {@link * #readPacket} as well as buffers allocated internally. * in the interest of speed, actual packet sizes and * internal buffer sizes may vary from this value. */ public PipeBuffer(int packetSize) { _packetSize = packetSize; } /** @return if {@link #closeRead} has been called */ public boolean isReadClosed() { return _gotReadEOF; } /** * Indicates that the "reader" of this PipeBuffer is finished. Calling this * method does not affect the operation of this PipeBuffer, it merely causes * {@link #isReadClosed} to return <code>true</code> from now on. Useful * for coordinating between reader and writer of the PipeBuffer. */ public void closeRead() { _gotReadEOF = true; } /** @return if {@link #closeWrite} has been called */ public boolean isWriteClosed() { return _gotWriteEOF; } /** * Indicates that the "writer" of this PipeBuffer is finished. Calling this * method does not affect the operation of this PipeBuffer, it merely causes * {@link #isWriteClosed} to return <code>true</code> from now on. Useful * for coordinating between reader and writer of the PipeBuffer. */ public void closeWrite() { _gotWriteEOF = true; } public int getPacketSize() { return _packetSize; } /** @return <code>true</code> if there are bytes to read in the buffer, <code>false</code> otherwise */ public boolean hasRemaining() { return(_totalBytes > 0); } /** @return the number of bytes which can be read from this buffer */ public long remaining() { return _totalBytes; } private void addLast(ByteWrapper bb) { _buffers.addLast(bb); } private void removeFirst(boolean canKeep) { // ditch the first buffer unless it is the only one left, we are allowed // to keep it, and it is at least as big as our packetSize if(!canKeep || (_buffers.size() != 1) || (_buffers.getFirst().capacity() < _packetSize)) { _buffers.removeFirst(); } else { _buffers.getFirst().clear(); } } /** * Reads a packet of data from this buffer. This method will always return * data unless there is no data at all in this buffer, in which case a * BufferUnderflowException will be thrown. As such, the actual size of the * returned packet may vary. */ public byte[] readPacket() { if(!hasRemaining()) { throw new BufferUnderflowException(); } ByteWrapper bb = _buffers.getFirst(); boolean canKeep = false; byte[] packet = null; if(bb.isFullToCapacity()) { // this buffer is completely full, just steal the internal buffer packet = bb.array(); } else { // this buffer is partially full, need to copy packet = new byte[bb.readRemaining()]; bb.read(packet, 0, packet.length); // since we copied the data, we can keep the buffer if desired canKeep = true; } removeFirst(canKeep); _totalBytes -= (long)packet.length; return packet; } /** * Reads the given number of bytes into the given buffer starting at the * given position. Throws a BufferUnderflowException if there are fewer * bytes in this buffer than the given length. */ public void read(byte[] buf, int pos, int len) { if(_totalBytes < (long)len) { throw new BufferUnderflowException(); } checkPositionAndLength(pos, len, buf); int origLen = len; while(len > 0) { ByteWrapper bb = _buffers.getFirst(); int numBytes = bb.read(buf, pos, len); pos += numBytes; len -= numBytes; if(!bb.hasReadRemaining()) { removeFirst(true); } } _totalBytes -= (long)origLen; } /** * Skips the given number of bytes in this buffer. Throws a * BufferUnderflowException if there are fewer bytes in this buffer than the * given length. */ public void skip(long len) { if(_totalBytes < len) { throw new BufferUnderflowException(); } // bogus value... if(len < 0) { throw new IllegalArgumentException("bogus length given"); } long origLen = len; while(len > 0) { ByteWrapper bb = _buffers.getFirst(); long numBytes = bb.skip(len); len -= numBytes; if(!bb.hasReadRemaining()) { removeFirst(true); } } _totalBytes -= origLen; } /** * Writes a packet of data to this buffer, where the initial data in the * packet will start at the given position and have the given length. * Regardless of the given length and position, the entire given buffer will * now be owned by this buffer and should never be used again by the caller. * Note, this call will never block. */ public void writePacket(byte[] buf, int pos, int len) { checkPositionAndLength(pos, len, buf); if(len > 0) { if((_totalBytes == 0) && (_buffers.size() > 0)) { // we have some empty buffers stashed away, but since we are writing a // packet, just ditch them _buffers.clear(); } // just slap it onto the end (should i copy small buffers?) addLast(new ByteWrapper(buf, pos, pos + len)); _totalBytes += (long)len; } } /** * Writes the given number of bytes from the given buffer starting at the * given position. Note, this call will never block. */ public void write(byte[] buf, int pos, int len) { checkPositionAndLength(pos, len, buf); int origLen = len; while(len > 0) { if((_buffers.isEmpty()) || (!_buffers.getLast().hasWriteRemaining())) { // we ran out of buffers, allocate new buffer addLast(new ByteWrapper(Math.max(_packetSize, len))); } ByteWrapper bb = _buffers.getLast(); int numBytes = bb.write(buf, pos, len); pos += numBytes; len -= numBytes; } _totalBytes += (long)origLen; } /** * Clears all remaining bytes from this buffer and releases all internal * buffers. */ public void clear() { _totalBytes = 0; _buffers.clear(); } /** * @return the number of "full" packets available in this buffer (where the * actual definition of "full" is up to this buffer). Regardless of * this value, if this buffer has bytes, they can be read by a call * to readPacket (although this may be less than efficient if * this value is 0). */ public int packetsAvailable() { // return the number of "full" buffers return((_buffers.size() > 0) ? (_buffers.getLast().isFullPacket(_packetSize) ? _buffers.size() : _buffers.size() - 1) : 0); } /** * @throws IllegalArgumentException if invalid position and length relative * to the given buffer. */ private static void checkPositionAndLength(int pos, int len, byte[] buf) { if((pos < 0) || (len < 0) || ((pos + len) > buf.length)) { throw new IllegalArgumentException("bogus position or length given"); } } /** * Utility class which sort of mimics a java.nio.ByteBuffer but doesn't suck * as much. Maintains both a read and write position so that you don't need * any of those silly and confusing flip/rewind/reset/etc. methods. */ private static class ByteWrapper { /** the current read position of this buffer, never > _writePosition */ private int _readPosition; /** the current write position of this buffer, never > _buf.length */ private int _writePosition; /** the actual data buffer */ private byte[] _buf; private ByteWrapper(int size) { this(new byte[size], 0, 0); } private ByteWrapper(byte[] buf, int readPosition, int writePosition) { _buf = buf; _readPosition = readPosition; _writePosition = writePosition; } public int capacity() { return _buf.length; } public byte[] array() { return _buf; } public int write(byte[] b, int pos, int len) { int numBytes = Math.min(writeRemaining(), len); System.arraycopy(b, pos, _buf, _writePosition, numBytes); _writePosition += numBytes; return numBytes; } public long skip(long len) { long numBytes = Math.min((long)readRemaining(), len); _readPosition += (int)numBytes; return numBytes; } public int read(byte[] b, int pos, int len) { int numBytes = Math.min(readRemaining(), len); System.arraycopy(_buf, _readPosition, b, pos, numBytes); _readPosition += numBytes; return numBytes; } public boolean hasWriteRemaining() { return(writeRemaining() > 0); } public int writeRemaining() { return(_buf.length - _writePosition); } public boolean hasReadRemaining() { return(readRemaining() > 0); } public int readRemaining() { return(_writePosition - _readPosition); } /** @return <code>true</code> if this buffer has as many bytes to read as its capacity, <code>false</code> otherwise */ public boolean isFullToCapacity() { return(readRemaining() == _buf.length); } /** @return <code>true</code> if the buffer has no more space for writing or at least the given packetSize number of bytes, <code>false</code> otherwise */ public boolean isFullPacket(int packetSize) { return(!hasWriteRemaining() || (readRemaining() >= packetSize)); } public void clear() { _readPosition = _writePosition = 0; } } /** * PacketInputStream implementation which reads from a PipeBuffer. One * hiccup here is that the single byte {@link #read()} method may fail if an * attempt is made to read a byte when none are in the PipeBuffer (as this * method is supposed to block in that case and this implementation has no * facility for blocking), so avoid that method call if at all * possible. Note, implementation is not synchronized. */ public static class InputStreamAdapter extends PacketInputStream { /** buffer for single byte read calls */ private SingleByteAdapter _singleByteAdapter = new SingleByteAdapter(); /** PipeBuffer to read from */ private PipeBuffer _buffer; public InputStreamAdapter() { this(PacketInputStream.DEFAULT_PACKET_SIZE); } public InputStreamAdapter(int packetSize) { super(packetSize); } public PipeBuffer getBuffer() { return _buffer; } public void setBuffer(PipeBuffer newBuffer) { _buffer = newBuffer; } /** * Returns the PipeBuffer of this InputStreamAdapter, creating if * necessary. If this method creates the PipeBuffer, it will be created * with the packet size of this PacketInputStream. The returned * PipeBuffer will be held onto internally for continued use by this * InputStreamAdapter. */ public PipeBuffer createPipeBuffer() { if(_buffer == null) { _buffer = new PipeBuffer(getPacketSize()); } return _buffer; } /** * Shares the PipeBuffer between this InputStreamAdapter and the given * OutputStreamAdapter. If this InputStreamAdapter and the given * OutputStreamAdapter already have different PipeBuffers, an IOException * will be thrown. Otherwise, if one of the two adapters already has a * PipeBuffer, it will be shared by both adapters. Otherwise, * {@link #createPipeBuffer} is called and the result is shared by both * adapters. */ public void connect(OutputStreamAdapter ostream) throws IOException { if((ostream.getBuffer() != null) && (getBuffer() != null) && (ostream.getBuffer() != getBuffer())) { throw new IOException( "Source and sink are already connected to other PipeBuffers"); } PipeBuffer pipeBuffer = ostream.getBuffer(); if(pipeBuffer == null) { pipeBuffer = getBuffer(); } if(pipeBuffer == null) { pipeBuffer = createPipeBuffer(); } ostream.setBuffer(pipeBuffer); setBuffer(pipeBuffer); } @Override public void close() { _buffer.closeRead(); } @Override public int available() throws IOException { return (int)_buffer.remaining(); } @Override public int read() throws IOException { if(!_buffer.hasRemaining() && !_buffer.isWriteClosed()) { throw new IOException( "Cannot call this method with no bytes in the PipeBuffer"); } return _singleByteAdapter.read(this); } @Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); } @Override public int read(byte[] buf, int pos, int len) throws IOException { if(isFinished()) { // all done return -1; } int numBytes = Math.min(len, (int)_buffer.remaining()); _buffer.read(buf, pos, numBytes); return numBytes; } @Override public byte[] readPacket(boolean readPartial) throws IOException { if(isFinished()) { // all done return null; } // readPartial is irrelevant because the PipeBuffer will automagically // do a partial read, and has no facility for blocking until an entire // packet is available return _buffer.readPacket(); } @Override public int packetsAvailable() throws IOException { return _buffer.packetsAvailable(); } @Override public long skip(long n) throws IOException { if(n <= 0) return 0; long toSkip = Math.min(n, _buffer.remaining()); _buffer.skip(toSkip); return toSkip; } /** * @return {@code true} if there will never be anymore bytes to read, * {@code false} otherwise. */ private boolean isFinished() { return(!_buffer.hasRemaining() && _buffer.isWriteClosed()); } } /** * PacketOutputStream implementation which writes to a PipeBuffer. By * default, this class will drop any bytes written after the reader has * closed. Note, implementation is not synchronized. */ public static class OutputStreamAdapter extends PacketOutputStream { /** buffer for single byte write calls */ private SingleByteAdapter _singleByteAdapter = new SingleByteAdapter(); /** PipeBuffer to write to */ private PipeBuffer _buffer; /** flag indicating how to handle write calls made when the reader has closed */ private boolean _throwOnReadClose; public OutputStreamAdapter() { this(false); } public OutputStreamAdapter(boolean throwOnReadClose) { _throwOnReadClose = throwOnReadClose; } public PipeBuffer getBuffer() { return _buffer; } public void setBuffer(PipeBuffer newBuffer) { _buffer = newBuffer; } public boolean getThrowOnReadClose() { return _throwOnReadClose; } /** * Sets the throwOnReadClose flag. Iff <code>true</code>, an exception * will be thrown if a write call is made when the reader has been closed. * Otherwise, write calls made when the reader is closed will just result * in dropped bytes. */ public void setThrowOnReadClose(boolean newThrowOnReadClose) { _throwOnReadClose = newThrowOnReadClose; } /** * Calls {@link PipeBuffer.InputStreamAdapter#connect} on the given * InputStreamAdapter with this OutputStreamAdapter as the parameter. */ public void connect(InputStreamAdapter istream) throws IOException { istream.connect(this); } @Override public void close() { _buffer.closeWrite(); } @Override public void write(int b) throws IOException { _singleByteAdapter.write(b, this); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int pos, int len) throws IOException { if(_buffer.isReadClosed()) { handleReadClosed(); // don't write if reader is finished (waste of resources) return; } _buffer.write(b, pos, len); } @Override public void writePacket(byte[] packet) throws IOException { if(_buffer.isReadClosed()) { handleReadClosed(); // don't write if reader is finished (waste of resources) return; } _buffer.writePacket(packet, 0, packet.length); } /** * Deals with a read closed situation while still writing. */ private void handleReadClosed() throws IOException { if(_throwOnReadClose) { throw new IOException("Reader is no longer reading"); } } } }