hudson.console.ConsoleNote.java Source code

Java tutorial

Introduction

Here is the source code for hudson.console.ConsoleNote.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2004-2010, Sun Microsystems, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.console;

import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.MarkupText;
import hudson.model.Describable;
import jenkins.model.Jenkins;
import hudson.model.Run;
import hudson.remoting.ObjectInputStreamEx;
import hudson.util.IOUtils;
import hudson.util.UnbufferedBase64InputStream;
import org.apache.commons.codec.binary.Base64OutputStream;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.tools.ant.BuildListener;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import com.jcraft.jzlib.GZIPInputStream;
import com.jcraft.jzlib.GZIPOutputStream;

/**
 * Data that hangs off from a console output.
 *
 * <p>
 * A {@link ConsoleNote} can be put into a console output while it's being written, and it represents
 * a machine readable information about a particular position of the console output.
 *
 * <p>
 * When Hudson is reading back a console output for display, a {@link ConsoleNote} is used
 * to trigger {@link ConsoleAnnotator}, which in turn uses the information in the note to
 * generate markup. In this way, we can overlay richer information on top of the console output.
 *
 * <h2>Comparison with {@link ConsoleAnnotatorFactory}</h2>
 * <p>
 * Compared to {@link ConsoleAnnotatorFactory}, the main advantage of {@link ConsoleNote} is that
 * it can be emitted into the output by the producer of the output (or by a filter), which can
 * have a much better knowledge about the context of what's being executed.
 *
 * <ol>
 * <li>
 * For example, when your plugin is about to report an error message, you can emit a {@link ConsoleNote}
 * that indicates an error, instead of printing an error message as plain text. The {@link #annotate(Object, MarkupText, int)}
 * method will then generate the proper error message, with all the HTML markup that makes error message
 * more user friendly.
 *
 * <li>
 * Or consider annotating output from Ant. A modified {@link BuildListener} can place a {@link ConsoleNote}
 * every time a new target execution starts. These notes can be then later used to build the outline
 * that shows what targets are executed, hyperlinked to their corresponding locations in the build output.
 * </ol>
 *
 * <p>
 * Doing these things by {@link ConsoleAnnotatorFactory} would be a lot harder, as they can only rely
 * on the pattern matching of the output.
 *
 * <h2>Persistence</h2>
 * <p>
 * {@link ConsoleNote}s are serialized and gzip compressed into a byte sequence and then embedded into the
 * console output text file, with a bit of preamble/postamble to allow tools to ignore them. In this way
 * {@link ConsoleNote} always sticks to a particular point in the console output.
 *
 * <p>
 * This design allows descendant processes of Hudson to emit {@link ConsoleNote}s. For example, Ant forked
 * by a shell forked by Hudson can put an encoded note in its stdout, and Hudson will correctly understands that.
 * The preamble and postamble includes a certain ANSI escape sequence designed in such a way to minimize garbage
 * if this output is observed by a human being directly.
 *
 * <p>
 * Because of this persistence mechanism, {@link ConsoleNote}s need to be serializable, and care should be taken
 * to reduce footprint of the notes, if you are putting a lot of notes. Serialization format compatibility
 * is also important, although {@link ConsoleNote}s that failed to deserialize will be simply ignored, so the
 * worst thing that can happen is that you just lose some notes.
 *
 * <h2>Behaviour, JavaScript, and CSS</h2>
 * <p>
 * {@link ConsoleNote} can have associated <tt>script.js</tt> and <tt>style.css</tt> (put them
 * in the same resource directory that you normally put Jelly scripts), which will be loaded into
 * the HTML page whenever the console notes are used. This allows you to use minimal markup in
 * code generation, and do the styling in CSS and perform the rest of the interesting work as a CSS behaviour/JavaScript.
 *
 * @param <T>
 *      Contextual model object that this console is associated with, such as {@link Run}.
 *
 * @author Kohsuke Kawaguchi
 * @see ConsoleAnnotationDescriptor
 * @see Functions#generateConsoleAnnotationScriptAndStylesheet()
 * @since 1.349
 */
public abstract class ConsoleNote<T> implements Serializable, Describable<ConsoleNote<?>>, ExtensionPoint {
    /**
     * When the line of a console output that this annotation is attached is read by someone,
     * a new {@link ConsoleNote} is de-serialized and this method is invoked to annotate that line.
     *
     * @param context
     *      The object that owns the console output in question.
     * @param text
     *      Represents a line of the console output being annotated.
     * @param charPos
     *      The character position in 'text' where this annotation is attached.
     *
     * @return
     *      if non-null value is returned, this annotator will handle the next line.
     *      this mechanism can be used to annotate multiple lines starting at the annotated position. 
     */
    public abstract ConsoleAnnotator annotate(T context, MarkupText text, int charPos);

    public ConsoleAnnotationDescriptor getDescriptor() {
        return (ConsoleAnnotationDescriptor) Jenkins.getInstance().getDescriptorOrDie(getClass());
    }

    /**
     * Prints this note into a stream.
     *
     * <p>
     * The most typical use of this is {@code n.encodedTo(System.out)} where stdout is connected to Hudson.
     * The encoded form doesn't include any new line character to work better in the line-oriented nature
     * of {@link ConsoleAnnotator}.
     */
    public void encodeTo(OutputStream out) throws IOException {
        // atomically write to the final output, to minimize the chance of something else getting in between the output.
        // even with this, it is still technically possible to get such a mix-up to occur (for example,
        // if Java program is reading stdout/stderr separately and copying them into the same final stream.)
        out.write(encodeToBytes().toByteArray());
    }

    /**
     * Prints this note into a writer.
     *
     * <p>
     * Technically, this method only works if the {@link Writer} to {@link OutputStream}
     * encoding is ASCII compatible.
     */
    public void encodeTo(Writer out) throws IOException {
        out.write(encodeToBytes().toString());
    }

    private ByteArrayOutputStream encodeToBytes() throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(buf));
        try {
            oos.writeObject(this);
        } finally {
            oos.close();
        }

        ByteArrayOutputStream buf2 = new ByteArrayOutputStream();

        DataOutputStream dos = new DataOutputStream(new Base64OutputStream(buf2, true, -1, null));
        try {
            buf2.write(PREAMBLE);
            dos.writeInt(buf.size());
            buf.writeTo(dos);
        } finally {
            dos.close();
        }
        buf2.write(POSTAMBLE);
        return buf2;
    }

    /**
     * Works like {@link #encodeTo(Writer)} but obtain the result as a string.
     */
    public String encode() throws IOException {
        return encodeToBytes().toString();
    }

    /**
     * Reads a note back from {@linkplain #encodeTo(OutputStream) its encoded form}.
     *
     * @param in
     *      Must point to the beginning of a preamble.
     *
     * @return null if the encoded form is malformed.
     */
    public static ConsoleNote readFrom(DataInputStream in) throws IOException, ClassNotFoundException {
        try {
            byte[] preamble = new byte[PREAMBLE.length];
            in.readFully(preamble);
            if (!Arrays.equals(preamble, PREAMBLE))
                return null; // not a valid preamble

            DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
            int sz = decoded.readInt();
            byte[] buf = new byte[sz];
            decoded.readFully(buf);

            byte[] postamble = new byte[POSTAMBLE.length];
            in.readFully(postamble);
            if (!Arrays.equals(postamble, POSTAMBLE))
                return null; // not a valid postamble

            ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream(new ByteArrayInputStream(buf)),
                    Jenkins.getInstance().pluginManager.uberClassLoader);
            try {
                return (ConsoleNote) ois.readObject();
            } finally {
                ois.close();
            }
        } catch (Error e) {
            // for example, bogus 'sz' can result in OutOfMemoryError.
            // package that up as IOException so that the caller won't fatally die.
            throw new IOException(e);
        }
    }

    /**
     * Skips the encoded console note.
     */
    public static void skip(DataInputStream in) throws IOException {
        byte[] preamble = new byte[PREAMBLE.length];
        in.readFully(preamble);
        if (!Arrays.equals(preamble, PREAMBLE))
            return; // not a valid preamble

        DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
        int sz = decoded.readInt();
        IOUtils.skip(decoded, sz);

        byte[] postamble = new byte[POSTAMBLE.length];
        in.readFully(postamble);
    }

    private static final long serialVersionUID = 1L;

    public static final String PREAMBLE_STR = "\u001B[8mha:";
    public static final String POSTAMBLE_STR = "\u001B[0m";

    /**
     * Preamble of the encoded form. ANSI escape sequence to stop echo back
     * plus a few magic characters.
     */
    public static final byte[] PREAMBLE = PREAMBLE_STR.getBytes();
    /**
     * Post amble is the ANSI escape sequence that brings back the echo.
     */
    public static final byte[] POSTAMBLE = POSTAMBLE_STR.getBytes();

    /**
     * Locates the preamble in the given buffer.
     */
    public static int findPreamble(byte[] buf, int start, int len) {
        int e = start + len - PREAMBLE.length + 1;

        OUTER: for (int i = start; i < e; i++) {
            if (buf[i] == PREAMBLE[0]) {
                // check for the rest of the match
                for (int j = 1; j < PREAMBLE.length; j++) {
                    if (buf[i + j] != PREAMBLE[j])
                        continue OUTER;
                }
                return i; // found it
            }
        }
        return -1; // not found
    }

    /**
     * Removes the embedded console notes in the given log lines.
     *
     * @since 1.350
     */
    public static List<String> removeNotes(Collection<String> logLines) {
        List<String> r = new ArrayList<String>(logLines.size());
        for (String l : logLines)
            r.add(removeNotes(l));
        return r;
    }

    /**
     * Removes the embedded console notes in the given log line.
     *
     * @since 1.350
     */
    public static String removeNotes(String line) {
        while (true) {
            int idx = line.indexOf(PREAMBLE_STR);
            if (idx < 0)
                return line;
            int e = line.indexOf(POSTAMBLE_STR, idx);
            if (e < 0)
                return line;
            line = line.substring(0, idx) + line.substring(e + POSTAMBLE_STR.length());
        }
    }
}