
com.gc.iotools.stream.is.inspection.DiagnosticInputStream Maven / Gradle / Ivy
Show all versions of easystream Show documentation
package com.gc.iotools.stream.is.inspection;
/*
* Copyright (c) 2008, 2015 Gabriele Contini. This source code is released
* under the BSD License.
*/
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gc.iotools.stream.utils.LogUtils;
/**
*
* A decorating InputStream
that detects and log useful debug
* informations about the stream passed in the constructor, and detects wrong
* usage patterns.
*
*
* InputStream
methods accessed after invocation of
* close()
.
* - Multiple invocations of
close()
method.
* - Missing
close()
invocation. Stream being garbage collected
* without close()
being called.
*
*
* It normally acts as a {@link FilterInputStream} simply forwarding all the
* calls to the InputStream
passed in the constructor, but also
* keeping track of the usage of the methods.
*
*
* Errors are both logged at WARN level and available through the standard class
* interface. Future version will allow the customization of this behavior
* disable the logging.
*
*
* It is designed to detect also errors that happens during object finalization,
* but to detect these errors in tests you must be very careful on your test
* design (see example). Errors in finalizers are available trough the
* {@linkplain #getFinalizationErrors()} method.
*
*
* It's an useful tool in unit tests to detect wrong handling of the streams,
* but it can be used in main applications too since it adds a very little
* overhead in standard situations.
*
*
* Sample Usage (in Junit 4):
*
*
* @org.junit.Test
* public void testWarnDoubleClose() throws Exception {
* InputStream myTestData = ....
* DiagnosticInputStream<InputStream> diagIs =
* new DiagnosticInputStream<InputStream>(myTestData);
* //The class and the method under test
* MyClassUnderTest.myMethodUnderTest(diagIs);
* final String[] instanceWarnings = diagIs.getInstanceWarnings();
* assertTrue("No problems" + diagIs.getStatusMessage(),
* instanceWarnings.length == 0);
* }
*
*
*
*
* If your code free resources in finalize()
methods, or the
* libraries you use do so you must use a more complex testing strategy because
* the references to to the active DiagnosticInputStream
instance
* in your Junit prevents the class from being garbage collected. See the wiki
* for details and {@linkplain #getFinalizationErrors()}.
*
*
* @since 1.2.6
* @author dvd.msnt
* @param
* Type of the wrapped (contained) InputStream
.
* @version $Id: DiagnosticInputStream.java 523 2013-01-02 15:46:17Z
* $
*/
public class DiagnosticInputStream extends
FilterInputStream {
private static int defaultLogDepth = 2;
private static final Logger LOGGER = LoggerFactory
.getLogger(DiagnosticInputStream.class);
private static final Collection STATIC_WARNINGS = new ArrayList();
/**
*
* Returns an array of descriptions of finalization errors. For instance
* when the stream is finalized but it was not closed.
*
*
* @return Description of finalization erros as an array of
* {@link java.lang.String} objects.
*/
public static String[] getFinalizationErrors() {
return STATIC_WARNINGS.toArray(new String[STATIC_WARNINGS.size()]);
}
/**
*
* resetFinalizationErrors
*
*/
public static void resetFinalizationErrors() {
STATIC_WARNINGS.clear();
}
/**
*
* Setter for the field defaultLogDepth
.
*
*
* @param defaultFrameDepth
* a int.
*/
public static void setDefaultLogDepth(final int defaultFrameDepth) {
DiagnosticInputStream.defaultLogDepth = defaultFrameDepth;
}
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
private int closeCount = 0;
private String closeTrace;
private final String constructorTrace;
/*
* Number of stack frames to log.
*/
private final int logDepth;
private boolean methodCalledAfterClose = false;
private final Collection warnings = new ArrayList();
/**
*
* Constructor for DiagnosticInputStream.
*
*
* @param in
* the source InputStream.
*/
public DiagnosticInputStream(final T in) {
this(in, defaultLogDepth);
}
/**
*
* Constructor for DiagnosticInputStream.
*
*
* @param inputStream
* the source InputStream
* @param logDepth
* Number of stack frames to log. It overrides the default static
* value.
*/
public DiagnosticInputStream(final T inputStream, final int logDepth) {
super(inputStream);
if (inputStream == null) {
throw new IllegalArgumentException(
"InputStream passed in the constructor is null");
}
if (logDepth <= 0) {
throw new IllegalArgumentException("Required logDepth is ["
+ logDepth + "] but it must be >0");
}
this.logDepth = logDepth;
this.constructorTrace = LogUtils.getCaller(getClass(), logDepth);
}
/** {@inheritDoc} */
@Override
public int available() throws IOException {
checkCloseInvoked("available");
return super.available();
}
private void checkCloseInvoked(final String methodName) {
if (this.closeCount > 0) {
this.methodCalledAfterClose = true;
final String warning = "ALREADY_CLOSED: ["
+ methodName
+ "] called by ["
+ LogUtils.getCaller(DiagnosticInputStream.class,
this.logDepth) + "]";
this.warnings.add(warning);
LOGGER.warn(warning + "but the stream was already closed by ["
+ this.closeTrace + "]");
}
}
/**
*
* clearInstanceWarnings
*
*/
public void clearInstanceWarnings() {
this.warnings.clear();
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
if (this.closeCount == 0) {
this.closeTrace = LogUtils.getCaller(DiagnosticInputStream.class,
this.logDepth);
} else {
final String warning = "MULTIPLE_CLOSE : method "
+ this.getClass().getSimpleName()
+ ".close() is being called the["
+ this.closeCount
+ "]time by ["
+ LogUtils.getCaller(DiagnosticInputStream.class,
this.logDepth) + "]";
LOGGER.warn(warning + " but stream was already closed by ["
+ this.closeTrace + "]");
this.warnings.add(warning);
}
this.closeCount++;
super.close();
}
/** {@inheritDoc} */
@Override
public void finalize() throws Throwable {
if (this.closeCount == 0) {
final String msg = "NOT_CLOSED : Finalizing ["
+ this.getClass().getSimpleName()
+ "] but close was not called yet. Wrapping class["
+ this.in.getClass().getSimpleName() + "]";
this.warnings.add(msg);
LOGGER.warn(msg + " Constructor trace:" + this.constructorTrace);
}
try {
super.finalize();
} finally {
if (this.warnings.size() > 0) {
final String statusString = getStatusMessage();
LOGGER.warn(statusString);
final String methodName = getConstructorCallerMethod();
STATIC_WARNINGS.add(methodName + " : " + statusString);
}
}
}
/**
* Returns the number of times that close was called on this stream.
*
* @return number of times that close was called on this stream.
*/
public int getCloseCount() {
return this.closeCount;
}
private String getConstructorCallerMethod() {
return this.constructorTrace.substring(
this.constructorTrace.indexOf('.') + 1,
this.constructorTrace.indexOf(':'));
}
/**
*
* Return the current captured bytes, if capture was enabled.
*
*
* The capture buffer might be truncated if maxCapture is set.
*
*
* @since 1.2.9
* @return the current captured bytes.
*/
public byte[] getContent() {
return this.baos.toByteArray();
}
/**
*
* getInstanceWarnings
*
*
* @return an array of {@link java.lang.String} objects.
*/
public String[] getInstanceWarnings() {
return this.warnings.toArray(new String[this.warnings.size()]);
}
/**
*
* Returns a string representation of the usage errors of the stream until
* now. Null if no error happened yet.
*
*
* @return String message that represents the errors, null if no error.
*/
public String getStatusMessage() {
String result = null;
if (this.warnings.size() > 0) {
final StringBuffer resultb = new StringBuffer(getClass()
.getSimpleName());
resultb.append(" constructed by [" + this.constructorTrace + "] ");
if (this.closeCount > 0) {
resultb.append("closed by: [" + this.closeTrace
+ "] has warnings:");
}
boolean first = true;
for (final String warning : this.warnings) {
resultb.append(warning);
resultb.append(first ? "" : "-----------");
first = false;
}
result = resultb.toString();
}
return result;
}
/**
*
* Returns the wrapped (original) InputStream
passed in the
* constructor. Any calls made to the returned stream will not be tracked by
* DiagnosticInputStream
, so this method should be used with
* care, and close()
and read()
methods should'nt
* be called on the returned InputStream
. Instead these methods
* should be called on DiagnosticInputStream
that simply
* forwards them to the underlying stream.
*
*
* @return The original InputStream
passed in the constructor
*/
public T getWrappedInputStream() {
@SuppressWarnings("unchecked")
final T result = (T) super.in;
return result;
}
/**
*
* isMethodCalledAfterClose
*
*
* @return a boolean.
*/
public boolean isMethodCalledAfterClose() {
return this.methodCalledAfterClose;
}
/** {@inheritDoc} */
@Override
public synchronized void mark(final int readlimit) {
checkCloseInvoked("mark");
super.mark(readlimit);
}
/** {@inheritDoc} */
@Override
public boolean markSupported() {
checkCloseInvoked("markSupported");
return super.markSupported();
}
/** {@inheritDoc} */
@Override
public int read() throws IOException {
checkCloseInvoked("read()");
return super.read();
}
/** {@inheritDoc} */
@Override
public int read(final byte[] b) throws IOException {
checkCloseInvoked("read(byte[])");
return super.read(b);
}
/** {@inheritDoc} */
@Override
public int read(final byte[] b, final int off, final int len)
throws IOException {
checkCloseInvoked("read(byte[],int,int)");
return super.read(b, off, len);
}
/** {@inheritDoc} */
@Override
public synchronized void reset() throws IOException {
checkCloseInvoked("reset");
super.reset();
}
/** {@inheritDoc} */
@Override
public long skip(final long n) throws IOException {
checkCloseInvoked("skip");
return super.skip(n);
}
}