/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* 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 au.gov.ga.earthsci.application.console;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import au.gov.ga.earthsci.common.util.TailByteArrayOutputStream;
/**
* Helper class that installs delegator output streams to intercept System.out
* and System.err calls. The streams delegate their methods to an internal list
* of output streams. The data is collected, up to a configurable limit, after
* which only the tail is kept.
* <p/>
* Callers can add output streams that will be written to whenever
* System.out/System.err are written to. The history will also optionally be
* written to the added streams.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public enum StandardOutputCollector
{
INSTANCE;
public static final int DEFAULT_LIMIT = 1024 * 1024; //1MB
private final OutputStreamSet outSet = new OutputStreamSet();
private final OutputStreamSet errSet = new OutputStreamSet();
private final PrintStream outStream = new PrintStream(outSet);
private final PrintStream errStream = new PrintStream(errSet);
private final OutputCollector collector = new OutputCollector();
private StandardOutputCollector()
{
collector.setLimit(DEFAULT_LIMIT);
}
/**
* Replace the System.out and System.err with those in this class, to
* capture the bytes written to them. Calls
* {@link System#setOut(PrintStream)} and {@link System#setErr(PrintStream)}
* with the sets from this class.
*/
public void install()
{
if (System.out != outStream)
{
outSet.add(System.out);
outSet.add(collector.out);
System.setOut(outStream);
}
if (System.err != errStream)
{
errSet.add(System.err);
errSet.add(collector.err);
System.setErr(errStream);
}
}
/**
* @return The limit of tail bytes to capture
*/
public int getLimit()
{
return collector.getLimit();
}
/**
* Set the limit of bytes to capture. The tail is always kept. Default value
* is {@value #DEFAULT_LIMIT}.
*
* @param limit
*/
public void setLimit(int limit)
{
collector.setLimit(limit);
}
/**
* Write the data collected by this collector to the given output streams.
*
* @param out
* Output stream to write the standard out data collected
* @param err
* Output stream to write the standard err data collected
* @throws IOException
*/
public void writeHistory(OutputStream out, OutputStream err) throws IOException
{
collector.writeToStreams(out, err);
}
/**
* Add the given streams as streams that are written to when standard out
* and standard error are written to. Can also optionally write all the data
* collected up until now.
*
* @param out
* Stream to write standard out to
* @param err
* Stream to write standard err to
* @param writeHistory
* Should the data collected until now be written immediately?
*/
public void addStreams(OutputStream out, OutputStream err, boolean writeHistory)
{
IOException exception = null;
synchronized (collector)
{
if (writeHistory)
{
try
{
writeHistory(out, err);
}
catch (IOException e)
{
//don't write anything to the console here, must write outside
//the synch block
exception = e;
}
}
//there's no chance for System.out to be written to between the above
//and below, due to the sychronization on the collector and the fact
//that the collector's write methods are also synchronized on itself
outSet.add(out);
errSet.add(err);
}
if (exception != null)
{
exception.printStackTrace();
}
}
/**
* Remove the given streams from the set of streams written to.
*
* @param out
* Output stream to remove
* @param err
* Error stream to remove
* @see #addStreams(OutputStream, OutputStream, boolean)
*/
public void removeStreams(OutputStream out, OutputStream err)
{
synchronized (collector)
{
outSet.remove(out);
errSet.remove(err);
}
}
/**
* Collects output from output and error streams into a byte array with a
* configurable limit. Keeps track of which sections of the byte array come
* from the output stream and which come from the error stream. Can rewrite
* the historic output/error data captured to another pair of streams, and
* will write in the same order that it was written to.
*/
private class OutputCollector extends TailByteArrayOutputStream
{
private int[] switchIndices = new int[8];
private int switchIndicesCount = 0;
public OutputCollector()
{
setLimit(DEFAULT_LIMIT);
}
/**
* Write the historic stream data to the given output streams.
*
* @param out
* @param err
* @throws IOException
*/
protected synchronized void writeToStreams(OutputStream out, OutputStream err)
throws IOException
{
OutputStream current = out;
//find first index that is not before the start index
int startIndex = 0;
for (int i = 0; i < switchIndicesCount; i++)
{
if (switchIndices[i] >= start)
{
break;
}
startIndex++;
current = swap(out, err, current);
}
int previousIndex = start;
for (int i = startIndex; i < switchIndicesCount; i++)
{
int index = switchIndices[i];
int length = index - previousIndex;
if (length > 0)
{
current.write(buf, previousIndex, length);
current.flush();
}
previousIndex = index;
current = swap(out, err, current);
}
int length = count - previousIndex;
if (length > 0)
{
current.write(buf, previousIndex, length);
current.flush();
}
}
private OutputStream swap(OutputStream stream1, OutputStream stream2,
OutputStream current)
{
return current != stream1 ? stream1 : stream2;
}
@Override
protected synchronized void tailUpdated(int movement, int start, int count)
{
if (movement != 0)
{
//remove any pairs of indices that will both be negative
//after the movement:
int removeCount = 0;
for (int i = 1; i < switchIndicesCount; i += 2)
{
if (switchIndices[i] - movement >= 0)
{
//found a second index in the pair that will be
//positive, so don't remove
break;
}
removeCount += 2;
}
removeCount = Math.min(removeCount, switchIndicesCount);
System.arraycopy(switchIndices, removeCount, switchIndices, 0,
switchIndicesCount - removeCount);
switchIndicesCount -= removeCount;
//decrease the remainder of the indices by the movement amount
for (int i = 0; i < switchIndicesCount; i++)
{
switchIndices[i] -= movement;
}
}
}
private synchronized void switchToError(boolean error)
{
//an even number of indices means current mode is out,
//odd means current mode is error
if ((switchIndicesCount % 2 == 0) == error)
{
if (switchIndicesCount + 1 > switchIndices.length)
{
int[] newArray = new int[switchIndices.length << 1];
System.arraycopy(switchIndices, 0, newArray, 0, switchIndicesCount);
switchIndices = newArray;
}
switchIndices[switchIndicesCount++] = count;
}
}
/**
* {@link FilterOutputStream} that writes to the {@link OutputCollector}
* after switching it to non-error mode.
*/
public OutputStream out = new FilterOutputStream(this)
{
@Override
public void write(int b) throws IOException
{
switchToError(false);
out.write(b);
}
@Override
public void write(byte[] b) throws IOException
{
switchToError(false);
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException
{
switchToError(false);
out.write(b, off, len);
}
};
/**
* {@link FilterOutputStream} that writes to the {@link OutputCollector}
* after switching it to error mode.
*/
public OutputStream err = new FilterOutputStream(this)
{
@Override
public void write(int b) throws IOException
{
switchToError(true);
out.write(b);
}
@Override
public void write(byte[] b) throws IOException
{
switchToError(true);
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException
{
switchToError(true);
out.write(b, off, len);
}
};
}
}