/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* 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, or
* (at your option) any later version.
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.shell.io;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.List;
/**
* This class provides a buffered byte-stream pipeline implementation that
* supports multiple sources and sinks. The pipeline has a finite buffer,
* so a thread that reads and writes to the same pipeline risks deadlock.
* <p>
* Unlike the standard Piped* classes,
* Pipeline and its related classes do not try to detect dead pipes based
* on exit of threads. Instead, a pipeline shuts down when the sources
* are closed, or when {@link #shutdown()} method called.
* <p>
* The intended lifecycle of a Pipeline is as follows:
* <ol>
* <li>A Pipeline object is instantiated.</li>
* <li>One or more sources and sinks are created using {@link #createSource()}
* and {@link #createSink()}.
* <li>The Pipeline is activated by calling {@link #activate()}.
* <li>Data is written to the pipeline sources and read from the sinks.
* <li>The pipeline sources are closed, causing the pipeline to shut down cleanly.
* Read calls on the sinks will return any remaining buffered data and then
* signal EOF in the normal way.
* <li>The sinks are closed, and the Pipeline is shutdown.
* </ol>
*
* @author crawley@jnode.org
*/
public class Pipeline {
// FIXME This first-cut implementation unnecessarily double-copies data when
// a reader is waiting for it. It doesn't fill/empty the buffer in a
// circular fashion. If there are multiple active readers or writers,
// too many threads get woken up. Finally, this class doesn't implement
// atomic writes / reads or detect cases where behavior is non-deterministic.
private List<PipelineInputStream> sinks =
new ArrayList<PipelineInputStream>();
private List<PipelineOutputStream> sources =
new ArrayList<PipelineOutputStream>();
private byte[] buffer;
private int pos = 0;
private int lim = 0;
private int state = INITIAL;
private static final int INITIAL = 1;
private static final int ACTIVE = 2;
private static final int CLOSED = 4;
private static final int SHUTDOWN = 8;
private static final String[] STATE_NAMES = new String[] {
null, "INITIAL", "ACTIVE", null, "CLOSED",
null, null, null, "SHUTDOWN"
};
/**
* The default Pipeline buffer size.
*/
public static final int DEFAULT_BUFFER_SIZE = 1024;
/**
* Create a pipeline, in 'inactive' state with the default buffer size;
*/
public Pipeline() {
buffer = new byte[DEFAULT_BUFFER_SIZE];
}
/**
* Create a pipeline, in 'inactive' state.
* @param bufferSize the pipeline's buffer size.
*/
public Pipeline(int bufferSize) {
buffer = new byte[bufferSize];
}
/**
* Create a sink for a inactive pipeline.
* @return the sink.
* @throws IOException This is thrown if the pipeline is 'active' or 'shut down'.
*/
public synchronized PipelineInputStream createSink() throws IOException {
checkState(INITIAL, "create");
PipelineInputStream is = new PipelineInputStream(this);
sinks.add(is);
return is;
}
private void checkState(int allowedStates, String action) throws IOException {
if ((state & allowedStates) == 0) {
String stateName = STATE_NAMES[state];
throw new IOException(action + " not allowed in state " + stateName);
}
}
/**
* Create a source for a inactive pipeline.
* @return the source.
* @throws IOException This is thrown if the pipeline is 'active' or 'shut down'.
*/
public synchronized PipelineOutputStream createSource() throws IOException {
checkState(INITIAL, "create");
PipelineOutputStream os = new PipelineOutputStream(this);
sources.add(os);
return os;
}
/**
* Put the pipeline into the 'active' state.
* @throws IOException This is thrown if the pipeline is 'shut down', or
* if it is 'inactive' but there are no sources or sinks.
*/
public synchronized void activate() throws IOException {
checkState(INITIAL, "activate");
if (sinks.isEmpty() || sources.isEmpty()) {
throw new IOException("pipeline has no inputs and/or outputs");
}
state = ACTIVE;
}
/**
* Test if the pipeline is in the 'active' state.
* @return <code>true</code> if the pipeline is active.
*/
public synchronized boolean isActive() {
return state == ACTIVE;
}
/**
* Test if the pipeline is in the 'closed' state.
* @return <code>true</code> if the pipeline is closed.
*/
public synchronized boolean isClosed() {
return state == CLOSED;
}
/**
* Test if the pipeline is in the 'shut down' state.
* @return <code>true</code> if the pipeline is shut down.
*/
public synchronized boolean isShutdown() {
return state == SHUTDOWN;
}
/**
* Forcibly shut down the pipeline. This will cause any threads
* currently blocked on sources or sinks to get an IOException.
*/
public synchronized void shutdown() {
state = SHUTDOWN;
this.notifyAll();
}
synchronized int available() throws IOException {
checkState(ACTIVE, "available");
return lim - pos;
}
synchronized void closeInput(PipelineInputStream input) {
sinks.remove(input);
if (sinks.isEmpty()) {
if (state < CLOSED) {
state = CLOSED;
this.notifyAll();
}
}
}
synchronized void closeOutput(PipelineOutputStream output) {
sources.remove(output);
if (sources.isEmpty()) {
if (state < CLOSED) {
state = CLOSED;
this.notifyAll();
}
}
}
synchronized int read(byte[] b, int off, int len) throws IOException {
checkState(ACTIVE | CLOSED | SHUTDOWN, "read");
int startOff = off;
while (off < len && state <= CLOSED) {
while (pos == lim && state == ACTIVE) {
try {
this.wait();
} catch (InterruptedException ex) {
throw new InterruptedIOException();
}
}
if (pos == lim) {
break;
}
while (off < len && pos < lim) {
b[off++] = buffer[pos++];
}
if (pos == lim) {
pos = 0;
lim = 0;
}
this.notifyAll();
}
return (off == startOff) ? -1 : (off - startOff);
}
synchronized long skip(long n) throws IOException {
checkState(ACTIVE | CLOSED | SHUTDOWN, "skip");
long off = 0;
while (off < n && state <= CLOSED) {
while (pos == lim && state == ACTIVE) {
try {
this.wait();
} catch (InterruptedException ex) {
throw new InterruptedIOException();
}
}
if (pos == lim) {
break;
}
long count = Math.min(lim - pos, n - off);
pos += count;
if (pos == lim) {
pos = 0;
lim = 0;
}
this.notifyAll();
}
return off == 0 ? -1 : off;
}
synchronized void flush() throws IOException {
// FIXME This should be unnecessary ... but we'll do it for now to be safe.
this.notifyAll();
}
synchronized void write(byte[] b, int off, int len) throws IOException {
checkState(ACTIVE, "write");
while (off < len) {
while (lim == buffer.length) {
try {
this.wait();
checkState(ACTIVE, "write");
} catch (InterruptedException ex) {
throw new InterruptedIOException();
}
}
while (off < len && lim < buffer.length) {
buffer[lim++] = b[off++];
}
this.notifyAll();
}
}
}