package jenkins.util; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; /** * Filtering {@link OutputStream} that looks for {@link #MARK} in the output stream and notifies the callback. * * The mark itself will be removed from the stream. * * @author Kohsuke Kawaguchi * @since 1.458 */ public abstract class MarkFindingOutputStream extends OutputStream { private final OutputStream base; public MarkFindingOutputStream(OutputStream base) { this.base = base; } /** * Position in {@link #MARK} if we are currently suspecting a match. */ private int match = 0; public synchronized void write(int b) throws IOException { if (MBYTES[match] == b) {// another byte matched. Good. Keep going... match++; if (match == MBYTES.length) { // don't send MARK to the output, but instead notify the callback onMarkFound(); match = 0; } } else { if (match > 0) { // only matched partially. send the partial match that we held off down the pipe base.write(MBYTES, 0, match); match = 0; // this might match the first byte in MARK, so retry. write(b); } else { base.write(b); } } } public void write(byte b[], int off, int len) throws IOException { final int start = off; final int end = off + len; for (int i=off; i<end; ) { if (MBYTES[match] == b[i]) {// another byte matched. Good. Keep going... match++; i++; if (match == MBYTES.length) { base.write(b,off,i-off-MBYTES.length); // flush the portion up to MARK // don't send MARK to the output, but instead notify the callback onMarkFound(); match = 0; off = i; len = end-i; } } else { if (match > 0) { // only matched partially. // if a part of the partial match spans into the previous write, we need to fake that write. int extra = match-(i-start); if (extra>0) { base.write(MBYTES,0,extra); } match = 0; // this b[i] might be a fast byte in MARK, so we'll retry } else { // irrelevant byte. i++; } } } // if we are partially matching, can't send that portion yet. if (len-match>0) base.write(b, off, len-match); } public void flush() throws IOException { flushPartialMatch(); base.flush(); } public void close() throws IOException { flushPartialMatch(); base.close(); } private void flushPartialMatch() throws IOException { if (match>0) { base.write(MBYTES,0,match); match = 0; } } protected abstract void onMarkFound(); // having a new line in the end makes it work better with line-buffering transformation public static final String MARK = "[Jenkins:SYNC-MARK]\n"; private static final byte[] MBYTES = toUTF8(MARK); private static byte[] toUTF8(String s) { try { return s.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } }