/*
* Copyright 2006-2010 Brian S O'Neill
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.cojen.dirmi.io;
import java.io.InterruptedIOException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.cojen.dirmi.ClosedException;
/**
* Unbuffered replacement for {@link java.io.PipedOutputStream}. This piped
* stream does not have the flaws found in the java.io implementation. It
* allows multiple threads to write to it without interfering with the stream's
* state. Also, a thread reading from the stream can't get into a one-second
* polling mode.
*
* @author Brian S O'Neill
* @see PipedInputStream
*/
public class PipedOutputStream extends OutputStream {
private final Lock mLock;
private final Condition mReadCondition;
private final Condition mWriteCondition;
private PipedInputStream mPin;
private boolean mEverConnected;
private byte[] mData;
private int mOffset;
private int mLength;
private byte[] mTinyBuf;
public PipedOutputStream() {
mLock = new ReentrantLock();
mReadCondition = mLock.newCondition();
mWriteCondition = mLock.newCondition();
}
public PipedOutputStream(PipedInputStream pin) throws IOException {
mLock = pin.setOutput(this);
mReadCondition = mLock.newCondition();
mWriteCondition = mLock.newCondition();
setInput(pin);
}
public void write(int b) throws IOException {
mLock.lock();
try {
checkConnected();
byte[] bytes = mTinyBuf;
if (bytes == null) {
mTinyBuf = bytes = new byte[1];
}
bytes[0] = (byte) b;
mData = bytes;
mOffset = 0;
mLength = 1;
mReadCondition.signal();
mPin.notifyReady();
waitForWriteCompletion();
} finally {
mLock.unlock();
}
}
public void write(byte[] bytes) throws IOException {
write(bytes, 0, bytes.length);
}
public void write(byte[] bytes, int offset, int length) throws IOException {
if ((offset < 0) || (offset > bytes.length) || (length < 0) ||
((offset + length) > bytes.length) || ((offset + length) < 0))
{
throw new IndexOutOfBoundsException();
}
if (length <= 0) {
return;
}
mLock.lock();
try {
checkConnected();
mData = bytes;
mOffset = offset;
mLength = length;
mReadCondition.signal();
mPin.notifyReady();
waitForWriteCompletion();
} finally {
mLock.unlock();
}
}
// Caller must hold mLock.
private void waitForWriteCompletion() throws IOException {
try {
while (mData != null) {
mWriteCondition.await();
checkConnected();
}
} catch (InterruptedException e) {
mData = null;
throw new InterruptedIOException("Thread interrupted");
}
}
public boolean isReady() throws IOException {
mLock.lock();
try {
checkConnected();
return mData == null;
} finally {
mLock.unlock();
}
}
public boolean isClosed() {
mLock.lock();
try {
return mPin == null && mEverConnected;
} finally {
mLock.unlock();
}
}
public void close() {
mLock.lock();
try {
if (mPin != null) {
PipedInputStream pin = mPin;
mPin = null;
pin.outputClosed();
mReadCondition.signalAll();
mWriteCondition.signalAll();
pin.notifyClosed();
}
} finally {
mLock.unlock();
}
}
@Override
public String toString() {
String superStr = superToString();
mLock.lock();
try {
if (mPin == null) {
return superStr.concat(" (unconnected)");
} else {
return superStr + " connected to " + mPin.superToString();
}
} finally {
mLock.unlock();
}
}
String superToString() {
return super.toString();
}
// Caller must hold mLock.
int read() throws IOException {
waitForReadAvailable();
int offset = mOffset;
int b = mData[offset] & 0xff;
if (--mLength <= 0) {
mData = null;
mWriteCondition.signal();
} else {
mOffset = offset + 1;
}
return b;
}
// Caller must hold mLock.
int read(byte[] bytes, int offset, int length) throws IOException {
if ((offset < 0) || (offset > bytes.length) || (length < 0) ||
((offset + length) > bytes.length) || ((offset + length) < 0))
{
throw new IndexOutOfBoundsException();
}
int amt = 0;
waitForReadAvailable();
if (length >= mLength) {
length = mLength;
}
System.arraycopy(mData, mOffset, bytes, offset, length);
amt += length;
if ((mLength -= length) <= 0) {
mData = null;
mWriteCondition.signal();
} else {
mOffset += length;
}
return amt;
}
// Caller must hold mLock.
long skip(long n) throws IOException {
long amt = 0;
while (n > 0) {
waitForReadAvailable();
if (n <= mLength) {
amt += n;
if ((mLength -= n) <= 0) {
mData = null;
mWriteCondition.signal();
} else {
mOffset += n;
}
break;
}
amt += mLength;
n -= mLength;
mData = null;
mWriteCondition.signal();
}
return amt;
}
// Caller must hold mLock.
private void waitForReadAvailable() throws IOException {
try {
while (mData == null) {
mReadCondition.await();
checkConnected();
}
} catch (InterruptedException e) {
throw new InterruptedIOException("Thread interrupted");
}
}
// Caller must hold mLock.
int inputAvailable() throws IOException {
return (mData == null) ? 0 : mLength;
}
Lock setInput(PipedInputStream pin) throws IOException {
mLock.lock();
try {
if (mPin != null) {
throw new IOException("Already connected");
}
if (mEverConnected) {
throw new ClosedException();
}
mPin = pin;
mEverConnected = true;
} finally {
mLock.unlock();
}
return mLock;
}
// Caller must hold mLock.
private void checkConnected() throws IOException {
if (mPin == null) {
if (mEverConnected) {
throw new ClosedException();
}
throw new IOException("Not connected");
}
}
}