com.googlecode.kevinarpe.papaya.process.ProcessOutputStreamSettings Maven / Gradle / Ivy
package com.googlecode.kevinarpe.papaya.process;
/*
* #%L
* This file is part of Papaya.
* %%
* Copyright (C) 2013 - 2014 Kevin Connor ARPE ([email protected])
* %%
* Papaya is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GPL Classpath Exception:
* This project is subject to the "Classpath" exception as provided in
* the LICENSE file that accompanied this code.
*
* Papaya 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Papaya. If not, see .
* #L%
*/
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.regex.Pattern;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.googlecode.kevinarpe.papaya.FuncUtils;
import com.googlecode.kevinarpe.papaya.StringUtils;
import com.googlecode.kevinarpe.papaya.annotation.FullyTested;
import com.googlecode.kevinarpe.papaya.appendable.AbstractSimplifiedAppendable;
import com.googlecode.kevinarpe.papaya.appendable.AppendablePrintLineWithPrefix;
import com.googlecode.kevinarpe.papaya.appendable.ByteAppendable;
import com.googlecode.kevinarpe.papaya.argument.IntArgs;
import com.googlecode.kevinarpe.papaya.argument.ObjectArgs;
/**
* This simple class holds the settings for child process output streams (either STDOUT or STDERR)
* before the child process starts. The parent class, {@link AbstractProcessSettings}, has
* two instances: one each for STDOUT and STDERR.
*
* This class may not be instantiated directly. Normally, only class
* {@link AbstractProcessSettings} creates instances.
*
* @author Kevin Connor ARPE ([email protected])
*/
@FullyTested
public class ProcessOutputStreamSettings {
static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
static final Pattern DEFAULT_SPLIT_REGEX = StringUtils.NEW_LINE_REGEX;
static final Appendable DEFAULT_CHAR_CALLBACK = null;
static final FuncUtils.Func0 DEFAULT_CHAR_CALLBACK_FACTORY = null;
static final ByteAppendable DEFAULT_BYTE_CALLBACK = null;
static final FuncUtils.Func0 DEFAULT_BYTE_CALLBACK_FACTORY = null;
static final boolean DEFAULT_IS_DATA_ACCUMULATED = false;
static final int DEFAULT_MAX_ACCUMULATED_DATA_BYTE_COUNT = -1;
private Charset _charset;
private Pattern _optSplitRegex;
private Appendable _optCharCallback;
private FuncUtils.Func0 _optCharCallbackFactory;
private ByteAppendable _optByteCallback;
private FuncUtils.Func0 _optByteCallbackFactory;
private boolean _isDataAccumulated;
private int _maxAccumulatedDataByteCount;
ProcessOutputStreamSettings() {
_charset = DEFAULT_CHARSET;
_optSplitRegex = DEFAULT_SPLIT_REGEX;
_optCharCallback = DEFAULT_CHAR_CALLBACK;
_optCharCallbackFactory = DEFAULT_CHAR_CALLBACK_FACTORY;
_optByteCallback = DEFAULT_BYTE_CALLBACK;
_optByteCallbackFactory = DEFAULT_BYTE_CALLBACK_FACTORY;
_isDataAccumulated = DEFAULT_IS_DATA_ACCUMULATED;
_maxAccumulatedDataByteCount = DEFAULT_MAX_ACCUMULATED_DATA_BYTE_COUNT;
}
/**
* Copies another instance.
*/
ProcessOutputStreamSettings(ProcessOutputStreamSettings x) {
ObjectArgs.checkNotNull(x, "x");
this._charset = x._charset;
this._optSplitRegex = x._optSplitRegex;
this._optCharCallback = x._optCharCallback;
this._optCharCallbackFactory = x._optCharCallbackFactory;
this._optByteCallback = x._optByteCallback;
this._optByteCallbackFactory = x._optByteCallbackFactory;
this._isDataAccumulated = x._isDataAccumulated;
this._maxAccumulatedDataByteCount = x._maxAccumulatedDataByteCount;
}
/**
* Sets the {@link Charset} used to convert bytes read from STDOUT or STDERR to
* {@link String}. The default value is {@link Charset#defaultCharset()}, and works
* correctly in the vast majority of use cases.
*
* This feature is only relevant if {@link #charCallback(Appendable)} is set to a
* non-{@code null} value.
*
* @return reference to {@code this}
*
* @throws NullPointerException
* if {@code cs} is {@code null}
*
* @see Charset#defaultCharset()
* @see #_charset
*/
public ProcessOutputStreamSettings charset(Charset cs) {
ObjectArgs.checkNotNull(cs, "cs");
_charset = cs;
return this;
}
/**
* Retrieves the {@link Charset} used to convert bytes read from STDOUT or STDERR to
* {@link String}. The initial value is {@link Charset#defaultCharset()}.
*
* This feature is only relevant if {@link #charCallback(Appendable)} is set to a
* non-{@code null} value.
*
* @see Charset#defaultCharset()
* @see #charset(Charset)
*/
public Charset charset() {
return _charset;
}
/**
* Sets the optional {@link Pattern} used to split incoming characters. The initial value
* is {@link StringUtils#NEW_LINE_REGEX}.
*
* If this feature is enabled and a character-based callback is set, as bytes are read from
* the child process stream handle, they are converted to {@link String} using
* {@link Charset} from {@link #charset()}. Then, the resulting {@code String} is then
* split using this regular expression using {@link Splitter}. Important note: The text
* matched by the regular expression is discarded! Remaining characters are saved between
* stream reads until the regular expression matches. When the child process terminates,
* all remaining characters are written to the character-based callback.
*
* If the description above confuses, follow this example:
*
* - Pattern is "\n" (UNIX newline)
* - Remaining text is ""
* - Read #1: {@code "abc\ndef"}
*
* - Prepend remaining text ({@code ""}): {@code "abc\ndef"}
* - Split to: {@code "abc"} and {@code "def"}
* - Write {@code "abc"} to char callback
* - Remaining text is {@code "def"}
*
* - Read #2: {@code "\nghi\n"}
*
* - Prepend remaining text ({@code "def"}): {@code "def\nghi\n"}
* - Split to: {@code "def"}, {@code "ghi"}, and {@code ""}
* - Write {@code "def"} to char callback
* - Then, write {@code "ghi"} to char callback
* - Remaining text is {@code ""}
*
* - Read #3: {@code "jkl"}
*
* - Prepend remaining text ({@code ""}): {@code "jkl"}
* - Split to: {@code "jkl"}
* - Write nothing to char callback
* - Remaining text is {@code "jkl"}
*
* - Read #4: {@code "mno"}
*
* - Prepend remaining text ({@code "jkl"}): {@code "jklmno"}
* - Split to: {@code "jklmno"}
* - Write nothing to char callback
* - Remaining text is {@code "jklmno"}
*
* - Child process terminates
* - Write "jklmno" to char callback
*
* This feature is only relevant if {@link #charCallback(Appendable)} is set to a
* non-{@code null} value.
*
* @param optRegex
*
* - optional regular expression to split incoming characters
* - May be {@code null}
* - Must not match the empty string
*
*
* @return reference to {@code this}
*
* @throws IllegalArgumentException
* if {@code optRegex} is non-{@code null} and matches the empty string
*
* @see #splitRegex()
* @see StringUtils#NEW_LINE_REGEX
* @see #charset(Charset)
* @see #charCallback(Appendable)
*/
public ProcessOutputStreamSettings splitRegex(Pattern optRegex) {
if (null != optRegex && optRegex.matcher("").matches()) {
throw new IllegalArgumentException(String.format(
"Argument 'optRegex' matches empty string: '%s'", optRegex));
}
_optSplitRegex = optRegex;
return this;
}
/**
* Retrieves the optional {@link Pattern} used to split incoming characters. The initial
* value is {@link StringUtils#NEW_LINE_REGEX}.
*
* This feature is only relevant if {@link #charCallback(Appendable)} is set to a
* non-{@code null} value.
*
* @return (optional) regular expression used to split incoming characters.
* May be {@code null}
*
* @see #splitRegex(Pattern)
* @see #charset()
* @see #charCallback()
*/
public Pattern splitRegex() {
return _optSplitRegex;
}
/**
* Sets the optional character-based callback for STDOUT or STDERR. As bytes are received
* from STDOUT or STDERR, they are converted to {@link String} using {@link Charset} from
* {@link #charset()}, then appended to this callback. The initial character-based
* callback is {@code null}.
*
* If incoming data needs to be processed in byte-based form, use
* {@link #byteCallback(ByteAppendable)}. The callbacks for character- and byte-based data
* are mutually exclusive. Neither, one, or both may be employed.
*
* It is not a requirement to set a callback to receive incoming data from STDOUT or
* STDERR. Alternatively, there is a setting to accumulate all incoming data from STDOUT
* or STDERR via {@link #isDataAccumulated(boolean)}. This feature is also independent of
* callbacks to process incoming data.
*
* A separate thread always is used by {@link Process2} to read STDOUT or STDERR. Thus,
* incoming data is appended to the callback from a different thread than that used to
* start the process. This may be important for specialised implementations of
* {@link Appendable}. As a special case, this method does not accept {@link StringBuilder},
* as it is not thread-safe. Use {@link StringBuffer} instead.
*
* If {@link ProcessBuilder2#redirectErrorStream()} is enabled, all data from STDERR is
* redirected to STDOUT. This may help to simplify processing logic, at the expense of
* distinguishing from STDOUT and STDERR data.
*
* The {@link Appendable} interface is somewhat cumbersome with three methods. There is a
* simplified version to reduce boilerplate code: {@link AbstractSimplifiedAppendable}.
* Also, there is a full sample implementation: {@link AppendablePrintLineWithPrefix}.
*
* If the character-based callback has state, such as {@link StringBuffer}, and multiple child
* processes are started from a single {@link ProcessBuilder2}, a factory should instead
* be set via {@link #charCallbackFactory(FuncUtils.Func0)}.
*
* All calls to this method, which do not throw exceptions, clear previous settings to
* {@link #charCallbackFactory(FuncUtils.Func0)}.
*
* To capture child process output and write to a file, consider using a {@link BufferedWriter}
* that wraps a {@link OutputStreamWriter}. This allows for full control over {@link Charset},
* with buffering for more efficient I/O.
*
* @param optCallback
*
* - (optional) Appendable reference to receive incoming data from STDOUT.
* - Must not be an instance of {@link StringBuilder}, which is not thread-safe.
* Instead use {@link StringBuffer}.
* - May be {@code null}.
*
*
* @return reference to {@code this}
*
* @throws IllegalArgumentException
* if {@code optCallback} is an instance of {@link StringBuilder}
*
* @see #charset(Charset)
* @see #splitRegex(Pattern)
* @see #charCallbackFactory(FuncUtils.Func0)
* @see #charCallback()
* @see #byteCallback(ByteAppendable)
* @see #isDataAccumulated(boolean)
* @see Process2#redirectErrorStream()
*/
public ProcessOutputStreamSettings charCallback(Appendable optCallback) {
if (optCallback instanceof StringBuilder) {
throw new IllegalArgumentException(String.format(
"Arg 'optCallback': Cannot be an instance of type StringBuilder, which is not thread-safe."
+ "%nUse class %s instead",
StringBuffer.class.getSimpleName()));
}
_optCharCallbackFactory = null;
_optCharCallback = optCallback;
return this;
}
/**
* Retrieves the character-based callback for STDOUT or STDERR. Initial value is
* {@code null}.
*
* @return may be {@code null}
*
* @see #charCallback(Appendable)
* @see #isDataAccumulated()
*/
public Appendable charCallback() {
return _optCharCallback;
}
/**
* Sets the factory to create a new character-based callback. The initial value is
* {@code null}.
*
* If the character-based callback has state, such as {@link StringBuffer}, and multiple child
* processes are started from a single {@link ProcessBuilder2}, a factory should instead
* be used.
*
* All calls to this method clear previous settings to {@link #charCallback(Appendable)}.
*
* @param optCallbackFactory
* optional character-based callback factory. May be {@code null}.
*
* @return reference to {@code this}
*
* @see #charset(Charset)
* @see #splitRegex(Pattern)
* @see #charCallback(Appendable)
* @see #charCallbackFactory()
* @see #byteCallback(ByteAppendable)
* @see #isDataAccumulated(boolean)
* @see Process2#redirectErrorStream()
*/
public ProcessOutputStreamSettings charCallbackFactory(
FuncUtils.Func0 optCallbackFactory) {
_optCharCallback = null;
_optCharCallbackFactory = optCallbackFactory;
return this;
}
/**
* Retrieves the character-based callback factory for STDOUT or STDERR. Initial value is
* {@code null}.
*
* @return may be {@code null}
*
* @see #charCallbackFactory(FuncUtils.Func0)
* @see #isDataAccumulated()
*/
public FuncUtils.Func0 charCallbackFactory() {
return _optCharCallbackFactory;
}
/**
* Sets the optional byte-based callback for STDOUT or STDERR. As bytes are received from
* STDOUT or STDERR, they are appended to this callback.
*
* If incoming data needs to be processed in character-based form, use
* {@link #charCallback(Appendable)}. The callbacks for character- and byte-based data are
* mutually exclusive. Neither, one, or both may be employed.
*
* It is not a requirement to set a callback to receive incoming data from STDOUT or
* STDERR. Alternatively, there is a setting to accumulate all incoming data via
* {@link #isDataAccumulated(boolean)}. This feature is also independent of callbacks to
* process incoming data. This means it is possible to process data from both STDOUT and
* STDERR simultaneously (and separately) in both character- and byte-based form. If the
* accumulator feature is enabled, after the process has started, call one of these methods
* to access the accumulated data:
*
* - STDOUT:
*
* - {@link Process2#stdoutDataAsByteArr()}
* - {@link Process2#stdoutDataAsString()}
* - {@link Process2#stdoutDataAsString(Charset)}
*
* - STDERR:
*
* - {@link Process2#stderrDataAsByteArr()}
* - {@link Process2#stderrDataAsString()}
* - {@link Process2#stderrDataAsString(Charset)}
*
*
* A separate thread always is used by {@link Process2} to read STDOUT or STDERR. Thus,
* incoming data is appended to the callback from a different thread than that used to
* start the process. This may be important for specialised implementations of
* {@link Appendable}.
*
* If {@link #isDataAccumulated()} is enabled, all data from STDERR is redirected to
* STDOUT or STDERR. This may help to simplify processing logic, at the expense of
* distinguishing from STDOUT and STDERR data.
*
* If the byte-based callback has state and multiple child processes are started from a single
* {@link ProcessBuilder2}, a factory should instead be set via
* {@link #byteCallbackFactory(FuncUtils.Func0)}.
*
* All calls to this method, which do not throw exceptions, clear previous settings to
* {@link #byteCallbackFactory(FuncUtils.Func0)}.
*
* @param optCallback
*
* - (optional) ByteAppendable reference to receive incoming data from STDOUT or
* STDERR.
* - May be {@code null}.
*
*
* @return reference to {@code this}
*
* @see #byteCallback()
* @see #isDataAccumulated(boolean)
* @see Process2#redirectErrorStream()
*/
public ProcessOutputStreamSettings byteCallback(ByteAppendable optCallback) {
_optByteCallbackFactory = null;
_optByteCallback = optCallback;
return this;
}
/**
* Retrieves the byte-based callback for STDOUT. Default value is {@code null}.
*
* @return may be {@code null}
*
* @see #byteCallback(ByteAppendable)
* @see #isDataAccumulated()
*/
public ByteAppendable byteCallback() {
return _optByteCallback;
}
/**
* Sets the factory to create a new byte-based callback. The initial value is {@code null}.
*
* If the byte-based callback has state and multiple child
* processes are started from a single {@link ProcessBuilder2}, a factory should instead
* be used.
*
* All calls to this method clear previous settings to {@link #byteCallback(ByteAppendable)}.
*
* @param optCallbackFactory
* optional byte-based callback factory. May be {@code null}.
*
* @return reference to {@code this}
*
* @see #charset(Charset)
* @see #splitRegex(Pattern)
* @see #charCallback(Appendable)
* @see #byteCallback(ByteAppendable)
* @see #byteCallbackFactory()
* @see #isDataAccumulated(boolean)
* @see Process2#redirectErrorStream()
*/
public ProcessOutputStreamSettings byteCallbackFactory(
FuncUtils.Func0 optCallbackFactory) {
_optByteCallback = null;
_optByteCallbackFactory = optCallbackFactory;
return this;
}
/**
* Retrieves the byte-based callback factory for STDOUT or STDERR. Initial value is
* {@code null}.
*
* @return may be {@code null}
*
* @see #byteCallbackFactory(FuncUtils.Func0)
* @see #isDataAccumulated()
*/
public FuncUtils.Func0 byteCallbackFactory() {
return _optByteCallbackFactory;
}
/**
* Sets whether or not the child process manager should accumulate incoming data from this
* stream (either STDOUT or STDERR). The default setting is disabled ({@code false}).
* This feature may be controller from the builder ({@link ProcessBuilder2}) or the child
* process ({@link Process2}). After the child process has started, it is still possible
* to enable or disable this feature.
*
* If this feature is enabled, it is crucial to also set
* {@link #maxAccumulatedDataByteCount(int)}. By default, an unlimited amount of data is
* accumulated. If a large number of processes are launched simultaneously and each
* outputs a large amount of data on STDOUT or STDERR, the parent Java Virtual Machine may
* easily exhaust available memory.
*
* If this feature is enabled, call one of these methods to access the accumulated data
* while the child process is running or after it has terminated:
*
* - STDOUT:
*
* - {@link Process2#stdoutDataAsByteArr()}
* - {@link Process2#stdoutDataAsString()}
* - {@link Process2#stdoutDataAsString(Charset)}
*
* - STDERR:
*
* - {@link Process2#stderrDataAsByteArr()}
* - {@link Process2#stderrDataAsString()}
* - {@link Process2#stderrDataAsString(Charset)}
*
*
*
* @param b
* if {@code true}, all incoming data from STDOUT is accumulated
*
* @return reference to {@code this}
*
* @see #isDataAccumulated()
* @see #maxAccumulatedDataByteCount(int)
* @see Process2#stdoutDataAsByteArr()
* @see Process2#stdoutDataAsString()
* @see Process2#stdoutDataAsString(Charset)
* @see Process2#stderrDataAsByteArr()
* @see Process2#stderrDataAsString()
* @see Process2#stderrDataAsString(Charset)
*/
public ProcessOutputStreamSettings isDataAccumulated(boolean b) {
_isDataAccumulated = b;
return this;
}
/**
* Retrieves the setting for the data accumulation feature for this stream (either STDOUT
* or STDERR). The default value is {@code false}.
*/
public boolean isDataAccumulated() {
return _isDataAccumulated;
}
/**
* Sets the maximum number of bytes to accumulate from this stream (either STDOUT or STDERR).
* This feature is only relevant if {@link #isDataAccumulated()} is enabled.
*
* It is very important to configure this feature in parallel with
* {@link #isDataAccumulated()}. It is easy for a errant process to produce gigabytes of data
* on STDOUT or STDERR and exhaust all available memory for the parent Java virtual machine.
*
* @param max
* any value except zero. Negative value implies this feature is disabled.
*
* @return reference to {@code this}
*
* @see #maxAccumulatedDataByteCount()
* @see #isDataAccumulated()
*/
public ProcessOutputStreamSettings maxAccumulatedDataByteCount(int max) {
IntArgs.checkNotExactValue(max, 0, "max");
_maxAccumulatedDataByteCount = max;
return this;
}
/**
* Retrieves the maximum number of bytes to accumulate from this stream (either STDOUT or
* STDERR). This feature is only relevant if {@link #isDataAccumulated()} is enabled.
*
* @return if negative, this feature is disabled
*
* @see #maxAccumulatedDataByteCount(int)
* @see #isDataAccumulated()
*/
public int maxAccumulatedDataByteCount() {
return _maxAccumulatedDataByteCount;
}
@Override
public String toString() {
String x = String.format(
"%s ["
+ "%n\tcharset()=(%s) '%s'"
+ "%n\tsplitRegex()='%s'"
+ "%n\tcharCallback()=%s"
+ "%n\tcharCallbackFactory()=%s"
+ "%n\tbyteCallback()=%s"
+ "%n\tbyteCallbackFactory()=%s"
+ "%n\tisDataAccumulated()=%s"
+ "%n\tmaxAccumulatedDataByteCount()=%d"
+ "%n\t]",
getClass().getSimpleName(),
_charset.getClass().getSimpleName(),
_charset,
(null == _optSplitRegex ? "null" : _optSplitRegex),
(null == _optCharCallback ? "null" : _optCharCallback.getClass().getSimpleName()),
(null == _optCharCallbackFactory
? "null" : _optCharCallbackFactory.getClass().getSimpleName()),
(null == _optByteCallback ? "null" : _optByteCallback.getClass().getSimpleName()),
(null == _optByteCallbackFactory
? "null" : _optByteCallbackFactory.getClass().getSimpleName()),
_isDataAccumulated,
_maxAccumulatedDataByteCount);
return x;
}
/**
* Since this object is mutable, apply caution when calling this method and interpreting
* the result.
*
* {@inheritDoc}
*/
@Override
public int hashCode() {
int x = Objects.hashCode(
_charset,
_optSplitRegex,
_optCharCallback,
_optCharCallbackFactory,
_optByteCallback,
_optByteCallbackFactory,
_isDataAccumulated,
_maxAccumulatedDataByteCount);
return x;
}
/**
* Since this object is mutable, apply caution when calling this method and interpreting
* the result.
*
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
boolean result = (this == obj);
// This also handles when obj == null.
if (!result && obj instanceof ProcessOutputStreamSettings) {
ProcessOutputStreamSettings other = (ProcessOutputStreamSettings) obj;
result =
(this._isDataAccumulated == other._isDataAccumulated)
&& (this._maxAccumulatedDataByteCount == other._maxAccumulatedDataByteCount)
&& Objects.equal(this._charset, other._charset)
&& Objects.equal(this._optSplitRegex, other._optSplitRegex)
&& Objects.equal(this._optCharCallback, other._optCharCallback)
&& Objects.equal(this._optCharCallbackFactory, other._optCharCallbackFactory)
&& Objects.equal(this._optByteCallback, other._optByteCallback)
&& Objects.equal(this._optByteCallbackFactory, other._optByteCallbackFactory)
;
}
return result;
}
}