/*******************************************************************************
* Copyright (c) 2013 Rene Schneider, GEBIT Solutions GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package de.gebit.integrity.runner.console.intercept;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import com.google.inject.Singleton;
/**
* Default implementation of a console output interceptor. Hooks the System.out and System.err streams to do its work,
* thus it is critical that there's only one instance of this service in a given JVM. This imlementation automatically
* hooks the streams when at least one target is registered, and unhooks them when the last target unregisters.
*
* @author Rene Schneider - initial API and implementation
*
*/
@Singleton
public class DefaultConsoleOutputInterceptor implements ConsoleOutputInterceptor {
/**
* A sync object used to synchronize interception target access.
*/
protected final Object targetSync = new Object();
/**
* The interception targets.
*/
protected List<ConsoleInterceptorTarget> targets = new ArrayList<ConsoleInterceptorTarget>();
/**
* The original STDOUT stream.
*/
protected final PrintStream stdout = System.out;
/**
* The interceptor stream for STDOUT.
*/
protected InterceptPrintStream interceptStdout;
/**
* The original STDERR stream.
*/
protected final PrintStream stderr = System.err;
/**
* The interceptor stream for STDERR.
*/
protected InterceptPrintStream interceptStderr;
/**
* Reevaluates whether the streams are to be captured at the time of calling, and hooks or unhooks the streams as
* necessary.
*/
protected void updateStreamCapture() {
if (targets.size() > 0 && interceptStdout == null) {
interceptStdout = new InterceptPrintStream(stdout, false);
System.setOut(interceptStdout);
interceptStderr = new InterceptPrintStream(stderr, true);
System.setErr(interceptStderr);
} else if (targets.size() == 0 && interceptStdout != null) {
System.setOut(stdout);
interceptStdout = null;
System.setErr(stderr);
interceptStderr = null;
}
}
@Override
public void registerTarget(ConsoleInterceptorTarget aTarget) {
synchronized (targetSync) {
if (!targets.contains(aTarget)) {
targets.add(aTarget);
updateStreamCapture();
}
}
}
@Override
public void unregisterTarget(ConsoleInterceptorTarget aTarget) {
synchronized (targetSync) {
if (targets.contains(aTarget)) {
targets.remove(aTarget);
updateStreamCapture();
}
}
}
@Override
public void printlnStdErr(String aLine) {
stderr.println(aLine);
stderr.flush();
}
@Override
public void printStdErr(String aText) {
stderr.print(aText);
}
@Override
public void printlnStdOut(String aLine) {
stdout.println(aLine);
stdout.flush();
}
@Override
public void printStdOut(String aText) {
stdout.print(aText);
}
/**
* This stream is the core of this service: it captures all data being printed through it, splits it into single
* lines and forwards the lines to all targets.
*
*
* @author Rene Schneider - initial API and implementation
*
*/
protected class InterceptPrintStream extends PrintStream {
/**
* Whether this interceptor is capturing STDERR.
*/
private boolean stdErr;
/**
* The currently captured line.
*/
private volatile StringBuilder currentLine = new StringBuilder();
/**
* Flag used to prevent data captured in the print() methods from being captured again after it was encoded to
* bytes and passed on to the write() methods. Theoretically it would be sufficient to just hook the write
* methods, but data captured from those must be converted back to strings again, which is rather inefficient
* and can create encoding problems.
*/
private boolean writingStringData;
/**
* Creates a new instance.
*
* @param aTarget
* the actual stream which is to receive everything
* @param anStdErrFlag
* whether this stream is used to intercept stderr
*/
public InterceptPrintStream(PrintStream aTarget, boolean anStdErrFlag) {
super(aTarget);
stdErr = anStdErrFlag;
}
/**
* Flushes the current line to the targets, splitting it into single lines in the process. This does not flush
* if the line is entirely empty, but it will flush incomplete lines as well.
*/
public void flushBufferedLine() {
try {
synchronized (currentLine) {
if (currentLine.length() > 0) {
String tempCurrentLine = currentLine.toString();
currentLine = new StringBuilder();
String[] tempSplitted = tempCurrentLine.split("(\\r\\n)|(\\r)|(\\n)");
synchronized (targetSync) {
for (String tempPart : tempSplitted) {
for (ConsoleInterceptorTarget tempTarget : targets) {
tempTarget.onLine(tempPart, stdErr);
}
}
}
}
}
// SUPPRESS CHECKSTYLE IllegalCatch
} catch (Throwable exc) {
// caught here to prevent errors in the interceptor from harming the intercepted application
exc.printStackTrace();
}
}
private void appendToCurrentLine(String aText) {
try {
synchronized (currentLine) {
currentLine.append(aText);
flushIfNecessary();
}
// SUPPRESS CHECKSTYLE IllegalCatch
} catch (Throwable exc) {
// caught here to prevent errors in the interceptor from harming the intercepted application
exc.printStackTrace();
}
}
/**
* Flushes the current line to the targets, but only if it ends with a newline and can thus be considered
* "complete".
*/
public void flushIfNecessary() {
try {
synchronized (currentLine) {
if (currentLine.length() > 0) {
char tempLastChar = currentLine.charAt(currentLine.length() - 1);
if (tempLastChar == '\r' || tempLastChar == '\n') {
flushBufferedLine();
}
}
}
// SUPPRESS CHECKSTYLE IllegalCatch
} catch (Throwable exc) {
// caught here to prevent errors in the interceptor from harming the intercepted application
exc.printStackTrace();
}
}
@Override
public void println(String aLine) {
writingStringData = true;
try {
super.println(aLine);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println() {
writingStringData = true;
try {
super.println();
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(Object anObject) {
writingStringData = true;
try {
super.println(anObject);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(boolean aBoolean) {
writingStringData = true;
try {
super.println(aBoolean);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(char aChar) {
writingStringData = true;
try {
super.println(aChar);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(char[] someChars) {
writingStringData = true;
try {
super.println(someChars);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(double aDouble) {
writingStringData = true;
try {
super.println(aDouble);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(float aFloat) {
writingStringData = true;
try {
super.println(aFloat);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(int anInteger) {
writingStringData = true;
try {
super.println(anInteger);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void println(long aLong) {
writingStringData = true;
try {
super.println(aLong);
flushIfNecessary();
} finally {
writingStringData = false;
}
}
@Override
public void print(String aString) {
writingStringData = true;
try {
appendToCurrentLine(aString);
super.print(aString);
} finally {
writingStringData = false;
}
}
@Override
public void print(char aChar) {
writingStringData = true;
try {
appendToCurrentLine(Character.toString(aChar));
super.print(aChar);
} finally {
writingStringData = false;
}
}
@Override
public void print(double aDouble) {
writingStringData = true;
try {
appendToCurrentLine(Double.toString(aDouble));
super.print(aDouble);
} finally {
writingStringData = false;
}
}
@Override
public void print(boolean aBoolean) {
writingStringData = true;
try {
appendToCurrentLine(Boolean.toString(aBoolean));
super.print(aBoolean);
} finally {
writingStringData = false;
}
}
@Override
public void print(int anInteger) {
writingStringData = true;
try {
appendToCurrentLine(Integer.toString(anInteger));
super.print(anInteger);
} finally {
writingStringData = false;
}
}
@Override
public void print(long aLong) {
writingStringData = true;
try {
appendToCurrentLine(Long.toString(aLong));
super.print(aLong);
} finally {
writingStringData = false;
}
}
@Override
public void print(Object anObject) {
writingStringData = true;
try {
appendToCurrentLine("" + anObject);
super.print(anObject);
} finally {
writingStringData = false;
}
}
@Override
public void print(char[] someChars) {
writingStringData = true;
try {
appendToCurrentLine(Arrays.toString(someChars));
super.print(someChars);
} finally {
writingStringData = false;
}
}
@Override
public void print(float aFloat) {
writingStringData = true;
try {
appendToCurrentLine(Float.toString(aFloat));
super.print(aFloat);
} finally {
writingStringData = false;
}
}
@Override
public PrintStream printf(String aFormat, Object... someArgs) {
return super.printf(aFormat, someArgs);
}
@Override
public PrintStream printf(Locale aLocale, String aFormat, Object... someArgs) {
return super.printf(aLocale, aFormat, someArgs);
}
@Override
public PrintStream format(Locale aLocale, String aFormat, Object... someArgs) {
return super.format(aLocale, aFormat, someArgs);
}
@Override
public PrintStream format(String aFormat, Object... someArgs) {
return super.format(aFormat, someArgs);
}
@Override
public PrintStream append(char aChar) {
writingStringData = true;
try {
appendToCurrentLine(Character.toString(aChar));
return super.append(aChar);
} finally {
writingStringData = false;
}
}
@Override
public PrintStream append(CharSequence aSequence) {
writingStringData = true;
try {
appendToCurrentLine(aSequence == null ? "null" : aSequence.toString());
return super.append(aSequence);
} finally {
writingStringData = false;
}
}
@Override
public PrintStream append(CharSequence aSequence, int aStart, int anEnd) {
writingStringData = true;
try {
appendToCurrentLine(aSequence == null ? "null" : aSequence.subSequence(aStart, anEnd).toString());
return super.append(aSequence, aStart, anEnd);
} finally {
writingStringData = false;
}
}
@Override
public void write(int aByte) {
if (!writingStringData) {
appendToCurrentLine(Character.toString((char) aByte));
}
super.write(aByte);
}
@Override
public void write(byte[] someBytes, int anOffset, int aLength) {
if (!writingStringData) {
appendToCurrentLine(new String(someBytes, anOffset, aLength));
}
super.write(someBytes, anOffset, aLength);
}
}
}