Java tutorial
/* * 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; } }