/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Tiny Travel Tracker 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
package com.rareventure.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* This allows two threads to communicate through basically what amounts to a pipe.
* Bytes can be added by one thread and read from another. This class is thread safe.
*/
public class OutputStreamToInputStreamPipe extends InputStream
{
protected byte[] buffer;
protected int head;
protected int tail;
private int maxBufferSize = Integer.MAX_VALUE;
public static class PipeClosedException extends IOException
{
public PipeClosedException() {
super();
}
public PipeClosedException(String message, Throwable cause) {
super(message, cause);
}
public PipeClosedException(String detailMessage) {
super(detailMessage);
}
public PipeClosedException(Throwable cause) {
super(cause);
}
}
public OutputStreamToInputStreamPipe() {
this(32);
}
public OutputStreamToInputStreamPipe(int initialSize) {
if (initialSize <= 0) {
throw new IllegalArgumentException(
"The size must be greater than 0");
}
buffer = new byte[initialSize + 1];
head = 0;
tail = 0;
}
/**
* Creates an output stream that will write to this byte buffer (which
* can then be read from another thread)
*/
public OutputStream getOutputStreamToThisByteBuffer()
{
return new OutputStream() {
@Override
public void write(int oneByte) throws IOException {
OutputStreamToInputStreamPipe.this.add((byte) oneByte);
}
/**
* This closes but DOES NOT FLUSH. In other words,
* the input stream on the other end can still be not
* finished with the array when this is closed
*/
@Override
public void close() throws IOException {
OutputStreamToInputStreamPipe.this.finishWriting();
}
@Override
public void flush() throws IOException {
//this will throw an ioexception if input stream closes buffer
OutputStreamToInputStreamPipe.this.blockUntilEmpty();
}
@Override
public void write(byte[] buffer, int offset, int count)
throws IOException {
OutputStreamToInputStreamPipe.this.addBytes(buffer, offset, count);
}
@Override
public void write(byte[] buffer) throws IOException {
OutputStreamToInputStreamPipe.this.addBytes(buffer, 0, buffer.length);
}
};
}
public OutputStreamToInputStreamPipe(int initialSize, int maxBufferSize) {
this(initialSize);
this.maxBufferSize = maxBufferSize;
}
public synchronized int currBufferSize() {
int size = 0;
if (tail < head) {
size = buffer.length - head + tail;
} else {
size = tail - head;
}
return size;
}
public synchronized boolean isEmptyBuf() {
return (currBufferSize() == 0);
}
public synchronized void addBytes(byte[] bytes, int start, int len) throws PipeClosedException {
//while there isn't enough room
while (currBufferSize() + len >= buffer.length) {
if(closed)
throw new PipeClosedException("input stream closed pipe");
// if we are at the maximum buf size, wait for the reading thread to
// catch up
if (buffer.length >= maxBufferSize)
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
else {
// increase the size of the buffer
byte[] tmp = new byte[Math.min(((buffer.length - 1) * 2) + 1,
maxBufferSize)];
if (head > tail) {
System.arraycopy(buffer, head, tmp, 0, buffer.length - head);
System.arraycopy(buffer, 0, tmp, buffer.length - head, tail);
tail = tail + buffer.length - head;
head = 0;
} else {
System.arraycopy(buffer, head, tmp, 0, tail - head);
tail = tail - head;
head = 0;
}
buffer = tmp;
}
}
if(tail + len > buffer.length)
{
System.arraycopy(bytes, start, buffer, tail, buffer.length - tail);
System.arraycopy(bytes, start + buffer.length - tail, buffer, 0, len - (buffer.length - tail));
tail = len - (buffer.length - tail);
}
else
{
System.arraycopy(bytes, start, buffer, tail, len);
tail += len;
}
//notify any reader waiting that we have more bytes
notifyAll();
}
private byte[] tempByte1 = new byte[1];
private boolean doneWriting;
private boolean closed;
private Throwable pendingExceptionFromOutputStream;
public synchronized void add(final byte b) throws IOException {
tempByte1[0] = b;
addBytes(tempByte1,0,1);
}
@Override
public synchronized int read() throws IOException {
if(doneWriting && head == tail)
return -1;
read(tempByte1, 0, 1);
return tempByte1[0];
}
@Override
public synchronized int read(byte []data, int start, int inpLen) throws IOException
{
int actLen;
for(;;) {
if(pendingExceptionFromOutputStream != null)
throw new IOException("Output to this pipe had an exception", pendingExceptionFromOutputStream);
if(doneWriting && head == tail)
return -1;
if((actLen = Math.min(inpLen, this.currBufferSize())) != 0)
break;
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
if(head + actLen > buffer.length)
{
System.arraycopy(buffer, head, data, start, buffer.length - head);
System.arraycopy(buffer, 0, data, start + (buffer.length - head), actLen - (buffer.length - head));
head = actLen - (buffer.length - head);
}
else
{
System.arraycopy(buffer, head, data, start, actLen);
head += actLen;
}
//notify any writer blocked because the buffer filled completely up
// or any writer waiting for the buffer to be flushed
notifyAll();
return actLen;
}
/**
* Stream will close after the already written bytes are read.
*/
public synchronized void finishWriting()
{
doneWriting = true;
//notify any readers that we are done writing (writeTo(OutputStream) uses this)
notifyAll();
}
public synchronized void blockUntilClosed()
{
while(!closed)
{
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
/**
* Blocks until all the current bytes have been read.
* If input stream is closed, throws an IOException
* @throws IOException
*/
protected synchronized void blockUntilEmpty() throws PipeClosedException {
while(head != tail)
{
if(closed)
throw new PipeClosedException("input stream closed pipe");
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
@Override
public synchronized void close()
{
closed = true;
notifyAll();
}
public synchronized boolean atEof() {
return head == tail && doneWriting;
}
/**
* Writes all the data from os until eof to the output stream
* @param os
* @throws IOException
*/
public synchronized void writeTo(OutputStream os, int minWriteAmount) throws IOException {
while(!atEof())
{
int writeAmount = minWriteAmount;
//wait until the buffer is at least minWriteAmount
while(!doneWriting && currBufferSize() < minWriteAmount)
{
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
writeAmount = currBufferSize();
//we write amount minWriteAmount even if there is more data
//to prevent weird buffer amounts downstream of us
if(head + writeAmount > buffer.length)
{
os.write(buffer, head, buffer.length - head);
os.write(buffer, 0, writeAmount - (buffer.length - head));
head = writeAmount - (buffer.length - head);
}
else
{
os.write(buffer, head, writeAmount);
head += writeAmount;
}
}
}
/**
* Notifies the input stream that the output stream failed on the
* next input streams read
* @param e error
*/
public synchronized void notifyExceptionFromOutputStream(Throwable e) {
pendingExceptionFromOutputStream = e;
//notify any readers waiting for data
notifyAll();
}
}