/********************************************************************************
*
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2003, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 USA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* + Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* + Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
********************************************************************************/
package net.sourceforge.cruisecontrol.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.Logger;
/**
* Class which buffers stdout from a command (as {@link OutputStream} to which the data are written)
* and provides it to multiple readers as {@link InputStream} (see {@link StdoutBuffer#getContent()}).
* The buffer can be read as many times as required.
* <p>
* The {@link StdoutBuffer} filling and {@link StdoutBuffer#getContent()} stream reading operations are
* thread safe. However the individual methods of {@link StdoutBuffer} and {@link StdoutBuffer#getContent()}
* instance are not (they are supposed to be called within one thread)!
*
* @author <a href="mailto:dtihelka@kky.zcu.cz">Dan Tihelka</a>
*/
public class StdoutBuffer extends OutputStream {
static final String MSG_READER_ALREADY_CLOSED = "Reader already closed";
/**
* Constructor.
*
* @param logger the instance of Logger through which to log.
*/
public StdoutBuffer(Logger logger) {
log = logger;
buffer = new LinkedList<byte[]>();
chunkSize = 1000; /* 1000 bytes in each buffer item */
chunkBuffer = new ByteArrayOutputStream(chunkSize);
chunkWriter = dataEncoder(chunkBuffer);
} // StdoutBuffer
/**
* Implementation of {@link OutputStream#write(byte[])}
* Adds the given data to the buffer.
*/
@Override
public void write(byte[] data) throws IOException {
write(data, 0, data.length);
} // write
/**
* Implementation of {@link OutputStream#write(byte[], int, int)}
* Adds the given data to the buffer.
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
/* Cannot add when closed */
if (chunkBuffer == null) {
throw new IOException("Tried to add data when buffer is closed");
}
/* Chunk the array to write and write it to the buffer */
while (len > 0) {
int numWrite = Math.min(chunkSize - chunkBuffer.size(), len);
/* Write so many bytes to the custom stream to fill the chunker */
chunkWriter.write(b, off, numWrite);
/* Move in the array */
off += numWrite;
len -= numWrite;
/* Flush the chunker, if it is full */
if (chunkBuffer.size() >= chunkSize) {
flush();
}
}
} // write
/**
* Implementation of {@link OutputStream#write(int)}
*/
@Override
public void write(int b) throws IOException {
/* Cannot add when closed */
if (chunkBuffer == null) {
throw new IOException("Tried to add data when buffer is closed");
}
/* Write the byte to the custom stream to fill the chunker */
chunkWriter.write(b);
/* Flush the chunker, if it is full */
if (chunkBuffer.size() >= chunkSize) {
flush();
}
} // write
/**
* Implementation of {@link OutputStream#close()}
* <p>
* Closes the buffer, which signalizes that no more data will be written to the buffer.
* It is necessary for {@link InputStream} returned by {@link #getContent()} to signalize
* that all the data were read. Otherwise (the end of buffer is not known), reading from the
* stream would block forever.
*/
@Override
public void close() {
/* Already closed */
if (chunkBuffer == null) {
return;
}
/* Flush the custom stream to the chunker and close the custom stream */
try {
chunkWriter.flush();
chunkWriter.close();
} catch (IOException exc) { // not likely to happen ...
log.error("Error when closing chunk writer, the buffer will probably not be complete ...", exc);
}
/* Copy the content of chunker to the array of bytes */
synchronized (buffer) {
buffer.add(chunkBuffer.toByteArray());
buffer.add(null);
/* Notify all threads waiting for data */
buffer.notifyAll();
}
/* Release the chunker to allow freeing it - it will not be used anymore ... */
chunkBuffer = null;
chunkWriter = null;
} // close
/**
* Implementation of {@link OutputStream#flush()}..
*/
@Override
public void flush() {
/* Cannot flush when closed or chunker is empty */
if (chunkBuffer == null || chunkBuffer.size() == 0) {
return;
}
/* Copy the content of chunker to the array of bytes */
synchronized (buffer) {
buffer.add(chunkBuffer.toByteArray());
/* Notify all threads waiting for data */
buffer.notifyAll();
}
chunkBuffer.reset();
} // flush
/**
* Returns stream from which the content of the buffer can be read. The method can be called multiple times (as many
* times as wanted), always returning new reader reading buffer from the beginning.
* <p>
* Note that reading the stream in an independent thread is save (related to writing to the buffer
* from another thread), and it is highly recommended!
*
* @return the stream to read the buffer content.
* @throws IOException if the stream cannot be read.
*/
public InputStream getContent() throws IOException {
return dataDecoder(new BufferReader(buffer));
} // getContent
/**
* Gets the string representation of this buffer.
* @return the string representation.
*/
@Override
public String toString() {
return getClass().getName() + "[" + buffer.size() * chunkSize + " bytes in buffer (approx.)]";
} // toString
/*
* ----------- PROTECTED BLOCK -----------
*/
/**
* Output stream customizer. The data which are required to be written to the {@link #buffer}
* are passed through custom stream (or a sequence of streams) returned by this class. The
* data written to the returned stream must end up in the <code>stream</code>, from which
* they are read and stored in the {@link #buffer}:
*
* {@literal data -> stream[dataEncoder(OutputStream) -> {@link #buffer} ->
* -> stream[{@link #dataDecoder(InputStream)}] -> data
* }
* <p>
* This implementation returns back the <code>stream</code> instance.
* <p>
* It is ensured that this method is called prior to {@link #dataDecoder(InputStream)}.
*
* @param stream the stream into which the data are required to be written once
* passed through the custom stream.
* @return the custom stream through which to pass the data stored to #buffer.
* @see #dataDecoder(InputStream)
*/
protected OutputStream dataEncoder(OutputStream stream) {
return stream;
}
/**
* Input stream customizer. The data which are required to be read from {@link #buffer}
* are passed through custom stream (or a sequence of streams) returned by this class.
* The data in form {@link #buffer} are read through <code>stream</code> stream, and
* passed through dataDecoder(InputStream) stream. When read then, they
* must be in the same form as written to the stream returned by {@link #dataEncoder(OutputStream)}:
*
* {@literal data -> stream[{@link #dataEncoder(OutputStream)}] -> {@link #buffer} ->
* -> stream[dataDecoder(InputStream)] -> data
* }
* <p>
* This implementation returns back the <code>stream</code>.
*
* @param stream the stream into which the data are required to be written once
* passed through the custom stream.
* @return the custom stream through which to pass the data stored to #buffer.
* @see #dataEncoder(OutputStream)
*/
protected InputStream dataDecoder(InputStream stream) {
return stream;
}
/*
* ----------- ATTRIBS BLOCK -----------
*/
/**
* The array with all the data passed to the buffer through <code>write()</code> methods. The data
* are stored in the buffer here, and they can read many times through stream provided by
* {@link #getContent()}).
* <p>
* If the last item in the buffer is <code>null</code>, it signalizes that the whole buffer
* was filled and no more items will be added, see {@link #close()}.
* <p>
* The work with the variable MUST BE hold in critical section. However, items are added to the buffer
* only - once a chunk of bytes is in the buffer, it is neither changed not deleted.
*/
private final List<byte[]> buffer;
/**
* The size of buffer item chunk
*/
protected final int chunkSize;
/**
* The temporary buffer used for chunking the data. The data are first written to this buffer
* and when the buffer contains enough data for one chunk to be created, it is flushed to the
* main buffer.
*/
private ByteArrayOutputStream chunkBuffer;
/**
* The custom chunker stream returned by {@link #dataEncoder(java.io.OutputStream)}
*/
private OutputStream chunkWriter;
/**
* The logger
*/
protected Logger log;
/*
* ----------- INNER CLASSES -----------
*/
/**
* The stream reading data from the buffer.
*/
private final class BufferReader extends InputStream {
/**
* Constructor, sets to the beginning of stream.
*
* @param buffer the instance holding the buffered data.
* @throws IOException if the stream cannot be read.
*/
BufferReader(List<byte[]> buffer) throws IOException {
bufferInst = buffer;
reset();
} // BufferReader
/**
* Implementation of {@link InputStream#available()}
*/
@Override
public final int available() throws IOException {
/* Must not be closed */
if (isClosed) {
throw new IOException(MSG_READER_ALREADY_CLOSED);
}
int toread;
int last;
/* Compute the exact number of Bytes not read yet */
synchronized (bufferInst) {
last = bufferInst.size() - 1;
/* No data to read in buffer */
if (bufferInst.size() == chunkInd) {
return 0;
}
/* EOF reached */
if (last == chunkInd && bufferInst.get(last) == null) {
return -1;
}
/* Size of the current chunk */
toread = bufferInst.get(chunkInd).length - chunkPos;
/* Size of the chunks in the buffer */
for (int ind = chunkInd + 1; ind < last; ind++) {
toread += bufferInst.get(ind).length;
}
/* The last may be null */
if (bufferInst.get(last) != null) {
toread += bufferInst.get(last).length;
}
} // synchronized
/* Return the result */
return toread;
} // available
/**
* Implementation of {@link InputStream#close()}
*/
@Override
public final void close() {
isClosed = true;
} // close
/**
* Implementation of {@link InputStream#mark(int)}; does nothing
*/
@Override
public final void mark(int readlimit) {
/* Mark not supported */
} // mark
/**
* Implementation of InputStream#markSupported(); always returns <code>false</code>
*/
@Override
public final boolean markSupported() {
return false;
} // markSupported
/**
* Implementation of InputStream#read()
* @throws IOException if the stream cannot be read.
*/
@Override
public final int read() throws IOException {
byte[] data = new byte[1];
int numRead = read(data, 0, 1);
int out = data[0];
/* Return -1 when at the end of stream */
if (numRead < 0) {
return -1;
}
/* Convert byte to 0-255 range */
if (out < 0) {
out = (256 + out);
}
/* Return -1 when at the end of stream, or the value just read instead */
return out;
} // read
/**
* Implementation of {@link InputStream#read(byte[])}
* @throws IOException if the stream cannot be read.
*/
@Override
public final int read(byte[] outBuff) throws IOException {
return read(outBuff, 0, outBuff.length);
} // read
/**
* Implementation of {@link InputStream#read(byte[], int, int)}
* @throws IOException if the stream cannot be read.
*/
@Override
public final int read(byte[] outBuff, int from, int len) throws IOException {
byte[] currChunk;
int numRead = 0;
/* Must not be closed */
if (isClosed) {
throw new IOException(MSG_READER_ALREADY_CLOSED);
}
/* Read until the required number of bytes is read. */
while (numRead < len) {
/* Get the current buffer. */
synchronized (bufferInst) {
/* Bad state!!?? */
if (bufferInst.size() < chunkInd) {
throw new IOException("Reader outran the buffer?");
}
/* If nothing to read, wait until notified. If the required number of
* Bytes to read ('len' attribute) was get by available() method, it will
* not block */
if (bufferInst.size() == chunkInd) {
try {
bufferInst.wait();
} catch (InterruptedException tExc) {
log.error("Unexpected interruption when waiting for data", tExc);
return -1;
}
}
/* Get the current chunk. It cannot change once it is in the buffer */
currChunk = bufferInst.get(chunkInd);
} // synchronized
/* If the current chunk is empty, EOF was reached. If at least something was read, return the
* number of Bytes read. Otherwise return -1 */
if (currChunk == null) {
return numRead > 0 ? numRead : -1;
}
/* How many items from the current buffer to read */
int canRead = Math.min(len - numRead, currChunk.length - chunkPos);
/* Copy the number of bytes available in the current buffer */
System.arraycopy(currChunk, chunkPos, outBuff, from, canRead);
/* Shift the buffer position */
chunkPos += canRead;
numRead += canRead;
from += canRead;
/* Was the whole buffer read? Set the new if so */
if (chunkPos >= currChunk.length) {
chunkPos = 0;
chunkInd++;
}
}
/* Return what read */
return numRead;
} // read
/**
* Implementation of InputStream#reset()
* @throws IOException when the stream is closed.
*/
@Override
public final void reset() throws IOException {
/* Must not be closed */
if (isClosed) {
throw new IOException(MSG_READER_ALREADY_CLOSED);
}
chunkInd = 0;
chunkPos = 0;
} // reset
/**
* Implementation of InputStream#skip()
*/
@Override
public final long skip(long num) {
/* Skip is not provided now, reimplement by shifting in the buffer if required */
log.warn(StdoutBuffer.class.getName() + ".skip() is not implemented, ignoring request");
return 0;
} // skip
/**
* Gets the string representation of this reader.
* @return the string representation.
*/
@Override
public String toString() {
return getClass().getName() + "[" + chunkInd * chunkSize + " Bytes read from buffer with "
+ buffer.size() * chunkSize + " Bytes in the buffer]";
} // toString
/* ----------- ATTRIBS BLOCK ----------- */
/**
* The parent instance of the buffer from which the data are read
*/
private final List<byte[]> bufferInst;
/** Flag set when {@link #close()} is called. */
private boolean isClosed;
/**
* The index of the chunk to read
*/
private int chunkInd;
/**
* The index within the chunk to read
*/
private int chunkPos;
} // BufferReader
} // StdoutBuffer