/******************************************************************************* * * Copyright (c) 2004-2010, Oracle Corporation. * * 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 * * Contributors: * * * * *******************************************************************************/ package hudson.console; import hudson.MarkupText; import org.apache.commons.io.output.ProxyWriter; import org.kohsuke.stapler.framework.io.WriterOutputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * Used to convert plain text console output (as byte sequence) + embedded * annotations into HTML (as char sequence), by using {@link ConsoleAnnotator}. * * @param <T> Context type. * @author Kohsuke Kawaguchi * @since 1.349 */ public class ConsoleAnnotationOutputStream<T> extends LineTransformationOutputStream { private final Writer out; private final T context; private ConsoleAnnotator<T> ann; /** * Reused buffer that stores char representation of a single line. */ private final LineBuffer line = new LineBuffer(256); /** * {@link OutputStream} that writes to {@link #line}. */ private final WriterOutputStream lineOut; /** * */ public ConsoleAnnotationOutputStream(Writer out, ConsoleAnnotator<? super T> ann, T context, Charset charset) { this.out = out; this.ann = ConsoleAnnotator.cast(ann); this.context = context; this.lineOut = new WriterOutputStream(line, charset); } public ConsoleAnnotator getConsoleAnnotator() { return ann; } /** * Called after we read the whole line of plain text, which is stored in * {@link #buf}. This method performs annotations and send the result to * {@link #out}. */ protected void eol(byte[] in, int sz) throws IOException { line.reset(); final StringBuffer strBuf = line.getStringBuffer(); int next = ConsoleNote.findPreamble(in, 0, sz); List<ConsoleAnnotator<T>> annotators = null; { // perform byte[]->char[] while figuring out the char positions of the BLOBs int written = 0; while (next >= 0) { if (next > written) { lineOut.write(in, written, next - written); lineOut.flush(); written = next; } else { assert next == written; } // character position of this annotation in this line final int charPos = strBuf.length(); int rest = sz - next; ByteArrayInputStream b = new ByteArrayInputStream(in, next, rest); try { final ConsoleNote a = ConsoleNote.readFrom(new DataInputStream(b)); if (a != null) { if (annotators == null) { annotators = new ArrayList<ConsoleAnnotator<T>>(); } annotators.add(new ConsoleAnnotator<T>() { public ConsoleAnnotator annotate(T context, MarkupText text) { return a.annotate(context, text, charPos); } }); } } catch (IOException e) { // if we failed to resurrect an annotation, ignore it. LOGGER.log(Level.FINE, "Failed to resurrect annotation", e); } catch (ClassNotFoundException e) { LOGGER.log(Level.FINE, "Failed to resurrect annotation", e); } int bytesUsed = rest - b.available(); // bytes consumed by annotations written += bytesUsed; next = ConsoleNote.findPreamble(in, written, sz - written); } // finish the remaining bytes->chars conversion lineOut.write(in, written, sz - written); if (annotators != null) { // aggregate newly retrieved ConsoleAnnotators into the current one. if (ann != null) { annotators.add(ann); } ann = ConsoleAnnotator.combine(annotators); } } lineOut.flush(); MarkupText mt = new MarkupText(strBuf.toString()); if (ann != null) { ann = ann.annotate(context, mt); } out.write(mt.toString(true)); // this perform escapes } @Override public void flush() throws IOException { out.flush(); } @Override public void close() throws IOException { super.close(); out.close(); } /** * {@link StringWriter} enhancement that's capable of shrinking the buffer * size. * * <p> The problem is that {@link StringBuffer#setLength(int)} doesn't * actually release the underlying buffer, so for us to truncate the buffer, * we need to create a new {@link StringWriter} instance. */ private static class LineBuffer extends ProxyWriter { private LineBuffer(int initialSize) { super(new StringWriter(initialSize)); } private void reset() { StringBuffer buf = getStringBuffer(); if (buf.length() > 4096) { out = new StringWriter(256); } else { buf.setLength(0); } } private StringBuffer getStringBuffer() { StringWriter w = (StringWriter) out; return w.getBuffer(); } } private static final Logger LOGGER = Logger.getLogger(ConsoleAnnotationOutputStream.class.getName()); }