hudson.plugins.plot.PlotData.java Source code

Java tutorial

Introduction

Here is the source code for hudson.plugins.plot.PlotData.java

Source

/*
 * Copyright (c) 2007-2009 Yahoo! Inc.  All rights reserved.
 * The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

package hudson.plugins.plot;

import hudson.FilePath;
import hudson.model.AbstractProject;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.util.ChartUtil;
import hudson.util.ShiftedCategoryAxis;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Polygon;
import java.awt.Shape;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.logging.Logger;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.labels.StandardCategoryToolTipGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.DefaultDrawingSupplier;
import org.jfree.chart.plot.DrawingSupplier;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.AbstractCategoryItemRenderer;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import au.com.bytecode.opencsv.CSVReader;
import au.com.bytecode.opencsv.CSVWriter;

/**
 * A PlotData object represents the collection of data generated by one job
 * during its different build.
 * For each build the data collected according to the serie type will be recorded in a CSV file
 * stored at the root of the project. To produce the plot chart these values are read.
 */
public class PlotData implements Comparable<PlotData> {

    private static final Logger LOGGER = Logger.getLogger(PlotData.class.getName());
    /**
     * How the date of the build will be represented on the chart
     */
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM d");

    /**
     * Effectively a 2-dimensional array, where each row is the 
     * data for one data series of an individual build;  the columns
     * are: series y-value, series label, build number, optional URL
     */
    private ArrayList<String[]> rawPlotData = null;

    /**
     * The project (or job) that this plot belongs to.  A reference
     * to the project is needed to retrieve and save the CSV file
     * that is stored in the project's root directory.
     */
    private AbstractProject<?, ?> project;

    /**
     * The generated plot, which is only regenerated when new data
     * is added (it is re-rendered, however, every time it is requested).
     */
    private JFreeChart plot;

    /** All plots share the same JFreeChart drawing supplier object. */
    private static final DrawingSupplier supplier = new DefaultDrawingSupplier(
            DefaultDrawingSupplier.DEFAULT_PAINT_SEQUENCE, DefaultDrawingSupplier.DEFAULT_OUTLINE_PAINT_SEQUENCE,
            DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE, DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE,
            // the plot data points are a small diamond shape 
            new Shape[] { new Polygon(new int[] { 3, 0, -3, 0 }, new int[] { 0, 4, 0, -4 }, 4) });

    /** The default plot width. */
    private static final int DEFAULT_WIDTH = 750;

    /** The default plot height. */
    private static final int DEFAULT_HEIGHT = 450;

    /** The default number of builds on plot (all). */
    private static final int DEFAULT_NUMBUILDS = Integer.MAX_VALUE;

    /** The width of the plot. */
    private int width;

    /** The height of the plot. */
    private int height;

    /** The right-most build number on the plot. */
    private int rightBuildNum;

    /** Whether or not the plot has a legend. */
    private boolean hasLegend = true;

    /** Number of builds back to show on this plot from url. */
    public String urlNumBuilds;

    /** Title of plot from url. */
    public String urlTitle;

    /** Style of plot from url. */
    public String urlStyle;

    /** Use description flag from url. */
    public Boolean urlUseDescr;

    /** Title of plot. */
    public String title;

    /** Y-axis label. */
    public String yaxis;

    /** Array of data series. */
    public Series[] series;

    /** Group name that this plot belongs to. */
    public String group;

    /** 
     * Number of builds back to show on this plot. 
     * Empty string means all builds.  Must not be "0".
     */
    public String numBuilds;

    /**
     * The name of the CSV file that persists the plots data.
     * The CSV file is stored in the projects root directory.
     * This is different from the source csv file that can be used as a source for the plot.
     */
    public FilePath csvFilePath;

    /** Optional style of plot: line, line3d, stackedArea, stackedBar, etc. */
    public String style;

    /** Whether or not to use build descriptions as X-axis labels. Optional. */
    public boolean useDescr;

    /**
     * Construct a PlotData instance from a set of data stored in the CSV file
     * @param filePath The CSV file containing the data
     */
    public PlotData(AbstractProject<?, ?> project, FilePath filePath) {
        this.project = project;
        this.csvFilePath = filePath;
        // Restores the data fromt the CSV file (if it exists)
        loadData();
    }

    private String getURLTitle() {
        return urlTitle != null ? urlTitle : title;
    }

    public String getTitle() {
        return title;
    }

    /**
     * Set the title of this plot data
     * @param title
     */
    public void setTitle(String title) {
        this.title = title;
    }

    public String getCsvFileName() {
        return csvFilePath.getName();
    }

    /**
     * Sets the plot width from the "width" parameter in the
     * given StaplerRequest.  If the parameter doesn't exist
     * or isn't an integer then a default is used.
     */
    private void setWidth(StaplerRequest req) {
        String w = req.getParameter("width");
        if (w == null) {
            width = DEFAULT_WIDTH;
        } else {
            try {
                width = Integer.parseInt(w);
            } catch (NumberFormatException nfe) {
                width = DEFAULT_WIDTH;
            }
        }
    }

    private int getWidth() {
        return width;
    }

    /**
     * Sets the plot height from the "height" parameter in the
     * given StaplerRequest.  If the parameter doesn't exist
     * or isn't an integer then a default is used.
     */
    private void setHeight(StaplerRequest req) {
        String h = req.getParameter("height");
        if (h == null) {
            height = DEFAULT_HEIGHT;
        } else {
            try {
                height = Integer.parseInt(h);
            } catch (NumberFormatException nfe) {
                height = DEFAULT_HEIGHT;
            }
        }
    }

    private int getHeight() {
        return height;
    }

    private void setStyle(StaplerRequest req) {
        urlStyle = req.getParameter("style");
    }

    private String getUrlStyle() {
        return urlStyle != null ? urlStyle : (style != null ? style : "");
    }

    private void setUseDescr(StaplerRequest req) {
        String u = req.getParameter("usedescr");
        if (u == null) {
            urlUseDescr = null;
        } else {
            urlUseDescr = u.equalsIgnoreCase("on") || u.equalsIgnoreCase("true");
        }
    }

    private boolean getUrlUseDescr() {
        return urlUseDescr != null ? urlUseDescr : useDescr;
    }

    /**
     * Sets the number of builds to plot from the "numbuilds" parameter
     * in the given StaplerRequest.  If the parameter doesn't exist
     * or isn't an integer then a default is used.
     */
    private void setHasLegend(StaplerRequest req) {
        String legend = req.getParameter("legend");
        hasLegend = legend == null || legend.equalsIgnoreCase("on") || legend.equalsIgnoreCase("true");
    }

    public boolean hasLegend() {
        return hasLegend;
    }

    /**
     * Sets the number of builds to plot from the "numbuilds" parameter
     * in the given StaplerRequest.  If the parameter doesn't exist
     * or isn't an integer then a default is used.
     */
    private void setNumBuilds(StaplerRequest req) {
        urlNumBuilds = req.getParameter("numbuilds");
        if (urlNumBuilds != null) {
            try {
                // simply try and parse the string to see if it's a valid number, throw away the result.
            } catch (NumberFormatException nfe) {
                urlNumBuilds = null;
            }
        }
    }

    public String getURLNumBuilds() {
        return urlNumBuilds != null ? urlNumBuilds : numBuilds;
    }

    public String getNumBuilds() {
        return numBuilds;
    }

    public void setYaxis(String yaxis) {
        this.yaxis = yaxis;
    }

    public String getYaxis() {
        return yaxis;
    }

    /**
     * Sets the right-most build number shown on the plot from
     * the "rightbuildnum" parameter in the given StaplerRequest.
     * If the parameter doesn't exist or isn't an integer then 
     * a default is used.
     */
    private void setRightBuildNum(StaplerRequest req) {
        String build = req.getParameter("rightbuildnum");
        if (build == null) {
            rightBuildNum = Integer.MAX_VALUE;
        } else {
            try {
                rightBuildNum = Integer.parseInt(build);
            } catch (NumberFormatException nfe) {
                rightBuildNum = Integer.MAX_VALUE;
            }
        }
    }

    private int getRightBuildNum() {
        return rightBuildNum;
    }

    /**
     * For the Comparable interface
     * This will dictate the display order of the PlotData on the PlotReport page.
     * Title is used as the sorting criteria
     */
    public int compareTo(PlotData o) {
        return title.compareTo(o.getTitle());
    }

    /**
     * Add a new set of data out of one build to this plot data
     * Those data will be appended to the CSV file
     * @param seriesData
     * @param build
     * @param listener
     */
    public void addSeriesData(PlotPoint[] seriesData, Build<?, ?> build, BuildListener listener) {
        if (seriesData == null) {
            return;
        }

        for (PlotPoint plotPoint : seriesData) {
            rawPlotData.add(new String[] { plotPoint.getYvalue(), plotPoint.getLabel(), build.getNumber() + "",
                    String.valueOf(build.getTimeInMillis()), plotPoint.getUrl() });
        }
        // Persist the file to disk
        saveData();
    }

    /**
     * Reads the CSV file containing the plot data and stores everything in member variables
     */
    private void loadData() {
        // Deletes whatever could have been loaded previously
        rawPlotData = new ArrayList<String[]>();
        // Check the existence of the CSV file
        try {
            if (!csvFilePath.exists()) {
                return;
            }
        } catch (Exception e) {
            return;
        }
        // Read the file
        CSVReader reader = null;
        try {
            reader = new CSVReader(new InputStreamReader(csvFilePath.read()));
            String[] nextLine;
            // Read the title
            nextLine = reader.readNext();
            this.title = nextLine[1];

            nextLine = reader.readNext();
            if (nextLine[0].equals(Messages.Plot_Yaxis())) {
                this.yaxis = nextLine[1];

                // Throw away the next line
                reader.readNext();
            }

            // read each line of the CSV file and add to rawPlotData
            while ((nextLine = reader.readNext()) != null) {
                rawPlotData.add(nextLine);
            }
        } catch (IOException ioe) {
            //ignore
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException ignore) {
                    //ignore
                }
            }
        }
        return;
    }

    /**
     * Persists the data to disk into a CSV file at the root of the project
     * Cleaning up the workspace will not destroy these data
     */
    private void saveData() {
        LOGGER.info("Saving the plot data for Plot[" + title + "] from " + csvFilePath.getName());

        CSVWriter writer = null;
        try {
            writer = new CSVWriter(new OutputStreamWriter(csvFilePath.write()));
            // write 2 header lines
            String[] header1 = new String[] { Messages.Plot_Title(), title };
            String[] header2 = new String[] { Messages.Plot_Yaxis(), yaxis };
            String[] header3 = new String[] { Messages.Plot_Value(), Messages.Plot_SeriesLabel(),
                    Messages.Plot_BuildNumber(), Messages.Plot_BuildDate(), Messages.Plot_URL() };
            writer.writeNext(header1);// Title
            writer.writeNext(header2);// Yaxis
            writer.writeNext(header3);// Header

            // write each entry of rawPlotData to a new line in the CSV file
            for (String[] entry : rawPlotData) {
                if (project.getBuildByNumber(Integer.parseInt(entry[2])) != null) {
                    writer.writeNext(entry);
                }
            }
        } catch (Exception ioe) {
            //ignore
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException ignore) {
                    //ignore
                }
            }
        }
    }

    /**
      * Generates and writes the plot to the response output stream.
      * 
      * @param req the incoming request
      * @param rsp the response stream
      * @throws IOException
      */
    public void plotGraph(StaplerRequest req, StaplerResponse rsp) throws IOException {
        if (ChartUtil.awtProblemCause != null) {
            // Not available. Send out error message.
            rsp.sendRedirect2(req.getContextPath() + "/images/headless.png");
            return;
        }
        setWidth(req);
        setHeight(req);
        setNumBuilds(req);
        setRightBuildNum(req);
        setHasLegend(req);
        //        setTitle(req);
        setStyle(req);
        setUseDescr(req);
        // need to force regenerate the plot in case build
        // descriptions (used for tool tips) have changed 
        generatePlot(true);
        ChartUtil.generateGraph(req, rsp, plot, getWidth(), getHeight());
    }

    /**
     * Generates and writes the plot's clickable map to the response 
     * output stream.
     * 
     * @param req the incoming request
     * @param rsp the response stream
     * @throws IOException
     */
    public void plotGraphMap(StaplerRequest req, StaplerResponse rsp) throws IOException {
        if (ChartUtil.awtProblemCause != null) {
            // not available. send out error message
            rsp.sendRedirect2(req.getContextPath() + "/images/headless.png");
            return;
        }
        setWidth(req);
        setHeight(req);
        setNumBuilds(req);
        setRightBuildNum(req);
        setHasLegend(req);
        //        setTitle(req);
        setStyle(req);
        setUseDescr(req);
        generatePlot(false);
        ChartRenderingInfo info = new ChartRenderingInfo();
        plot.createBufferedImage(getWidth(), getHeight(), info);
        rsp.setContentType("text/plain;charset=UTF-8");
        rsp.getWriter().println(ChartUtilities.getImageMap(csvFilePath.getName(), info));
    }

    /**
     * Generates the plot and stores it in the plot instance variable.
     * 
     * @param forceGenerate if true, force the plot to be re-generated
     *        even if the on-disk data hasn't changed
     */
    private void generatePlot(boolean forceGenerate) {
        class Label implements Comparable<Label> {
            final private Integer buildNum;
            final private String buildDate;
            final private String text;

            public Label(String buildNum, String buildTime, String text) {
                this.buildNum = Integer.parseInt(buildNum);
                synchronized (DATE_FORMAT) {
                    this.buildDate = DATE_FORMAT.format(new Date(Long.parseLong(buildTime)));
                }
                this.text = text;
            }

            public int compareTo(Label that) {
                return this.buildNum - that.buildNum;
            }

            @Override
            public boolean equals(Object o) {
                return o instanceof Label && ((Label) o).buildNum.equals(buildNum);
            }

            @Override
            public int hashCode() {
                return buildNum.hashCode();
            }

            public String numDateString() {
                return "#" + buildNum + " (" + buildDate + ")";
            }

            @Override
            public String toString() {
                return text != null ? text : numDateString();
            }
        }
        LOGGER.fine("Generating plot from file: " + csvFilePath.getName());
        PlotCategoryDataset dataset = new PlotCategoryDataset();
        for (String[] record : rawPlotData) {
            // record: series y-value, series label, build number, build date, url
            int buildNum;
            try {
                buildNum = Integer.valueOf(record[2]);
                if (project.getBuildByNumber(buildNum) == null || buildNum > getRightBuildNum()) {
                    continue; // skip this record
                }
            } catch (NumberFormatException nfe) {
                continue; // skip this record all together
            }
            Number value = null;
            try {
                value = Integer.valueOf(record[0]);
            } catch (NumberFormatException nfe) {
                try {
                    value = Double.valueOf(record[0]);
                } catch (NumberFormatException nfe2) {
                    continue; // skip this record all together
                }
            }
            String series = record[1];
            Label xlabel = getUrlUseDescr() ? new Label(record[2], record[3], descriptionForBuild(buildNum))
                    : new Label(record[2], record[3], getBuildName(buildNum));
            String url = null;
            if (record.length >= 5)
                url = record[4];
            dataset.setValue(value, url, series, xlabel);
        }
        int numBuilds;
        try {
            numBuilds = Integer.parseInt(getURLNumBuilds());
        } catch (NumberFormatException nfe) {
            numBuilds = DEFAULT_NUMBUILDS;
        }
        dataset.clipDataset(numBuilds);
        plot = createChart(dataset);
        CategoryPlot categoryPlot = (CategoryPlot) plot.getPlot();
        categoryPlot.setDomainGridlinePaint(Color.black);
        categoryPlot.setRangeGridlinePaint(Color.black);
        categoryPlot.setDrawingSupplier(PlotData.supplier);
        CategoryAxis domainAxis = new ShiftedCategoryAxis(Messages.Plot_Build());
        categoryPlot.setDomainAxis(domainAxis);
        domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90);
        domainAxis.setLowerMargin(0.0);
        domainAxis.setUpperMargin(0.03);
        domainAxis.setCategoryMargin(0.0);
        for (Object category : dataset.getColumnKeys()) {
            Label label = (Label) category;
            if (label.text != null) {
                domainAxis.addCategoryLabelToolTip(label, label.numDateString());
            } else {
                domainAxis.addCategoryLabelToolTip(label, descriptionForBuild(label.buildNum));
            }
        }

        AbstractCategoryItemRenderer renderer = (AbstractCategoryItemRenderer) categoryPlot.getRenderer();
        int numColors = dataset.getRowCount();
        for (int i = 0; i < numColors; i++) {
            renderer.setSeriesPaint(i, new Color(Color.HSBtoRGB((1f / numColors) * i, 1f, 1f)));
        }
        renderer.setStroke(new BasicStroke(2.0f));
        renderer.setToolTipGenerator(new StandardCategoryToolTipGenerator(Messages.Plot_Build() + " {1}: {2}",
                NumberFormat.getInstance()));
        renderer.setItemURLGenerator(new PointURLGenerator());
        if (renderer instanceof LineAndShapeRenderer) {
            LineAndShapeRenderer lasRenderer = (LineAndShapeRenderer) renderer;
            lasRenderer.setShapesVisible(true); // TODO: deprecated, may be unnecessary
        }
    }

    /**
     * Creates a Chart of the style indicated by getEffStyle() using the given dataset.
     * Defaults to using createLineChart.
     */
    private JFreeChart createChart(PlotCategoryDataset dataset) {
        String s = getUrlStyle();
        if (s.equalsIgnoreCase("area")) {
            return ChartFactory.createAreaChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset,
                    PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("bar")) {
            return ChartFactory.createBarChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset,
                    PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("bar3d")) {
            return ChartFactory.createBarChart3D(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset,
                    PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("line3d")) {
            return ChartFactory.createLineChart3D(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset,
                    PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("stackedarea")) {
            return ChartFactory.createStackedAreaChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(),
                    dataset, PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("stackedbar")) {
            return ChartFactory.createStackedBarChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(),
                    dataset, PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("stackedbar3d")) {
            return ChartFactory.createStackedBarChart3D(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(),
                    dataset, PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        if (s.equalsIgnoreCase("waterfall")) {
            return ChartFactory.createWaterfallChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset,
                    PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
        }
        return ChartFactory.createLineChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset,
                PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false);
    }

    /**
     * Returns a trimmed description string for the build specified by the given build number.
     */
    private String descriptionForBuild(int buildNum) {
        Run<?, ?> r = project.getBuildByNumber(buildNum);
        if (r != null) {
            String tip = r.getTruncatedDescription();
            if (tip != null) {
                return tip.replaceAll("<p> *|<br> *", ", ");
            }
        }
        return null;
    }

    /**
     * Returns the trimmed display build string for the build specified by the given build number.
     */
    private String getBuildName(int buildNum) {
        Run<?, ?> r = project.getBuildByNumber(buildNum);
        if (r != null) {
            String tip = r.getDisplayName();
            if (tip != null) {
                return tip.replaceAll("<p> *|<br> *", ", ");
            }
        }
        return null;
    }
}