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.model.AbstractBuild; import hudson.model.AbstractProject; 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.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; 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.axis.LogarithmicAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; 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.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import au.com.bytecode.opencsv.CSVReader; import au.com.bytecode.opencsv.CSVWriter; /** * Represents the configuration for a single plot. A plot can have one or more * data series (lines). Each data series has one data point per build. The * x-axis is always the build number. * * A plot has the following characteristics: * <ul> * <li>a title (mandatory) * <li>y-axis label (defaults to no label) * <li>one or more data series * <li>plot group (defaults to no group) * <li>number of builds to show on the plot (defaults to all) * </ul> * * A plots group effects the way in which plots are displayed. Group names are * listed as links on the top-level plot page. The user then clicks on a group * and sees the plots that belong to that group. * * @author Nigel Daley */ public class Plot implements Comparable<Plot> { private static final Logger LOGGER = Logger.getLogger(Plot.class.getName()); 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 transient List<String[]> rawPlotData; /** * The generated plot, which is only regenerated when new data is added (it * is re-rendered, however, every time it is requested). */ private transient JFreeChart plot; /** * 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 transient AbstractProject<?, ?> project; /** 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 String DEFAULT_NUMBUILDS = ""; // Transient values /** The width of the plot. */ private transient int width; /** The height of the plot. */ private transient int height; /** The right-most build number on the plot. */ private transient int rightBuildNum; /** Whether or not the plot has a legend. */ private transient boolean hasLegend = true; /** Number of builds back to show on this plot from url. */ public transient String urlNumBuilds; /** Title of plot from url. */ public transient String urlTitle; /** Style of plot from url. */ public transient String urlStyle; /** Use description flag from url. */ public transient Boolean urlUseDescr; // Configuration values /** Title of plot. Mandatory. */ public String title; /** Y-axis label. Optional. */ public String yaxis; /** List of data series. */ public List<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 String csvFileName; /** The date of the last change to the CSV file. */ private long csvLastModification; /** 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; /** keep records for builds that was deleted */ private boolean keepRecords; /** Whether or not to exclude zero as default Y-axis value. Optional. */ public boolean exclZero; /** Use a logarithmic Y-axis. */ public boolean logarithmic; /** Min/max yaxis values, string used so if no value defaults used */ public String yaxisMinimum; public String yaxisMaximum; /** * Creates a new plot with the given paramenters. If numBuilds is the empty * string, then all builds will be included. Must not be zero. */ @DataBoundConstructor public Plot(String title, String yaxis, String group, String numBuilds, String csvFileName, String style, boolean useDescr, boolean keepRecords, boolean exclZero, boolean logarithmic, String yaxisMinimum, String yaxisMaximum) { this.title = title; this.yaxis = yaxis; this.group = group; this.numBuilds = numBuilds; this.csvFileName = csvFileName; this.style = style; this.useDescr = useDescr; this.keepRecords = keepRecords; this.exclZero = exclZero; this.logarithmic = logarithmic; this.yaxisMinimum = yaxisMinimum; this.yaxisMaximum = yaxisMaximum; } /** * @deprecated Kept for backward compatibility. */ @Deprecated public Plot(String title, String yaxis, String group, String numBuilds, String csvFileName, String style, boolean useDescr) { this(title, yaxis, group, numBuilds, csvFileName, style, useDescr, false, false, false, null, null); } // needed for serialization public Plot() { } public boolean getKeepRecords() { return keepRecords; } public boolean getExclZero() { return exclZero; } public boolean isLogarithmic() { return logarithmic; } public boolean hasYaxisMinimum() { return (getYaxisMinimum() != null); } public Double getYaxisMinimum() { return getDoubleFromString(yaxisMinimum); } public boolean hasYaxisMaximum() { return (getYaxisMaximum() != null); } public Double getYaxisMaximum() { return getDoubleFromString(yaxisMaximum); } public Double getDoubleFromString(String input) { Double result = null; if (input != null) { try { result = Double.parseDouble(input); } catch (NumberFormatException nfe) { // Not a problem, result already set } } return result; } public int compareTo(Plot o) { return title.compareTo(o.getTitle()); } @Override public String toString() { return "TITLE(" + getTitle() + "),YAXIS(" + yaxis + "),NUMSERIES(" + CollectionUtils.size(series) + "),GROUP(" + group + "),NUMBUILDS(" + numBuilds + "),RIGHTBUILDNUM(" + getRightBuildNum() + "),HASLEGEND(" + hasLegend() + "),ISLOGARITHMIC(" + isLogarithmic() + "),YAXISMINIMUM(" + yaxisMinimum + "),YAXISMAXIMUM(" + yaxisMaximum + "),FILENAME(" + getCsvFileName() + ")"; } public String getYaxis() { return yaxis; } public List<Series> getSeries() { return series; } public String getGroup() { return group; } public String getCsvFileName() { if (StringUtils.isBlank(csvFileName) && project != null) { try { csvFileName = File.createTempFile("plot-", ".csv", project.getRootDir()).getName(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Unable to create temporary CSV file.", e); } } return csvFileName; } /** * Sets the title for the plot from the "title" parameter in the given * StaplerRequest. */ private void setTitle(StaplerRequest req) { urlTitle = req.getParameter("title"); } private String getURLTitle() { return urlTitle != null ? urlTitle : title; } public String getTitle() { return title; } 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 = "on".equalsIgnoreCase(u) || "true".equalsIgnoreCase(u); } } 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 || "on".equalsIgnoreCase(legend) || "true".equalsIgnoreCase(legend); } 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. Integer.parseInt(urlNumBuilds); } catch (NumberFormatException nfe) { urlNumBuilds = null; } } } public String getURLNumBuilds() { return urlNumBuilds != null ? urlNumBuilds : numBuilds; } public String getNumBuilds() { return numBuilds; } /** * 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 (StringUtils.isBlank(build)) { rightBuildNum = Integer.MAX_VALUE; } else { try { rightBuildNum = Integer.parseInt(build); } catch (NumberFormatException nfe) { LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); rightBuildNum = Integer.MAX_VALUE; } } } private int getRightBuildNum() { return rightBuildNum; } /** * 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) { LOGGER.log(Level.SEVERE, "Exception converting to integer", 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) { LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); height = DEFAULT_HEIGHT; } } } private int getHeight() { return height; } public AbstractProject<?, ?> getProject() { return project; } /** * A reference to the project is needed to retrieve the project's root * directory where the CSV file is located. Unfortunately, a reference to * the project is not available when this object is created. * * @param project * the project */ public void setProject(AbstractProject<?, ?> project) { this.project = project; } /** * 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(getCsvFileName(), info)); } /** * Called when a build completes. Adds the finished build to this plot. This * method extracts the data for each data series from the build and saves it * in the plot's CSV file. * * @param build * @param logger */ public void addBuild(AbstractBuild<?, ?> build, PrintStream logger) { if (project == null) project = build.getProject(); // load the existing plot data from disk loadPlotData(); // extract the data for each data series for (Series series : getSeries()) { if (series == null) continue; List<PlotPoint> seriesData = series.loadSeries(build.getWorkspace(), build.getNumber(), logger); if (seriesData != null) { for (PlotPoint point : seriesData) { if (point == null) continue; rawPlotData.add(new String[] { point.getYvalue(), point.getLabel(), build.getNumber() + "", // convert to a string build.getTimestamp().getTimeInMillis() + "", point.getUrl() }); } } } // save the updated plot data to disk savePlotData(); } /** * 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 Label(String buildNum, String buildTime) { this(buildNum, buildTime, null); } 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.info("Determining if we should generate plot " + // getCsvFileName()); File csvFile = new File(project.getRootDir(), getCsvFileName()); if (csvFile.lastModified() == csvLastModification && plot != null && !forceGenerate) { // data hasn't changed so don't regenerate the plot return; } if (rawPlotData == null || csvFile.lastModified() > csvLastModification) { // data has changed or has not been loaded so load it now loadPlotData(); } // LOGGER.info("Generating plot " + getCsvFileName()); csvLastModification = csvFile.lastModified(); 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 (!reportBuild(buildNum) || buildNum > getRightBuildNum()) { continue; // skip this record } } catch (NumberFormatException nfe) { LOGGER.log(Level.SEVERE, "Exception converting to integer", 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) { LOGGER.log(Level.SEVERE, "Exception converting to number", 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]); String url = null; if (record.length >= 5) url = record[4]; dataset.setValue(value, url, series, xlabel); } String urlNumBuilds = getURLNumBuilds(); int numBuilds; if (StringUtils.isBlank(urlNumBuilds)) { numBuilds = Integer.MAX_VALUE; } else { try { numBuilds = Integer.parseInt(urlNumBuilds); } catch (NumberFormatException nfe) { LOGGER.log(Level.SEVERE, "Exception converting to integer", nfe); numBuilds = Integer.MAX_VALUE; } } dataset.clipDataset(numBuilds); plot = createChart(dataset); CategoryPlot categoryPlot = (CategoryPlot) plot.getPlot(); categoryPlot.setDomainGridlinePaint(Color.black); categoryPlot.setRangeGridlinePaint(Color.black); categoryPlot.setDrawingSupplier(Plot.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)); } } // Replace the range axis by a logarithmic axis if the option is // selected if (isLogarithmic()) { LogarithmicAxis logAxis = new LogarithmicAxis(getYaxis()); logAxis.setExpTickLabelsFlag(true); categoryPlot.setRangeAxis(logAxis); } // optionally exclude zero as default y-axis value ValueAxis rangeAxis = categoryPlot.getRangeAxis(); if ((rangeAxis != null) && (rangeAxis instanceof NumberAxis)) { if (hasYaxisMinimum()) { ((NumberAxis) rangeAxis).setLowerBound(getYaxisMinimum()); } if (hasYaxisMaximum()) { ((NumberAxis) rangeAxis).setUpperBound(getYaxisMaximum()); } ((NumberAxis) rangeAxis).setAutoRangeIncludesZero(!getExclZero()); } 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.setBaseStroke(new BasicStroke(2.0f)); renderer.setBaseToolTipGenerator(new StandardCategoryToolTipGenerator(Messages.Plot_Build() + " {1}: {2}", NumberFormat.getInstance())); renderer.setBaseItemURLGenerator(new PointURLGenerator()); if (renderer instanceof LineAndShapeRenderer) { String s = getUrlStyle(); LineAndShapeRenderer lasRenderer = (LineAndShapeRenderer) renderer; if ("lineSimple".equalsIgnoreCase(s)) { lasRenderer.setShapesVisible(false); // TODO: deprecated, may be unnecessary } else { 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 ("area".equalsIgnoreCase(s)) { return ChartFactory.createAreaChart(getURLTitle(), /* * categoryAxisLabel= */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("bar".equalsIgnoreCase(s)) { return ChartFactory.createBarChart(getURLTitle(), /* * categoryAxisLabel= */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("bar3d".equalsIgnoreCase(s)) { return ChartFactory.createBarChart3D(getURLTitle(), /* * categoryAxisLabel * = */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("line3d".equalsIgnoreCase(s)) { return ChartFactory.createLineChart3D(getURLTitle(), /* * categoryAxisLabel * = */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("lineSimple".equalsIgnoreCase(s)) { return ChartFactory.createLineChart(getURLTitle(), /*categoryAxisLabel=*/null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /*tooltips=*/true, /*url=*/false); } if ("stackedarea".equalsIgnoreCase(s)) { return ChartFactory.createStackedAreaChart(getURLTitle(), /* * categoryAxisLabel * = */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("stackedbar".equalsIgnoreCase(s)) { return ChartFactory.createStackedBarChart(getURLTitle(), /* * categoryAxisLabel * = */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("stackedbar3d".equalsIgnoreCase(s)) { return ChartFactory.createStackedBarChart3D(getURLTitle(), /* * categoryAxisLabel * = */null, getYaxis(), dataset, PlotOrientation.VERTICAL, hasLegend(), /* * tooltips * = */ true, /* url= */false); } if ("waterfall".equalsIgnoreCase(s)) { 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; } /** * Loads the plot data from the CSV file on disk. The CSV file is stored in * the projects root directory. The data is stored in the rawPlotData * instance variable. */ private void loadPlotData() { rawPlotData = new ArrayList<String[]>(); // load existing plot file File plotFile = new File(project.getRootDir(), getCsvFileName()); if (!plotFile.exists()) { return; } CSVReader reader = null; rawPlotData = new ArrayList<String[]>(); try { reader = new CSVReader(new FileReader(plotFile)); // throw away 2 header lines reader.readNext(); reader.readNext(); // read each line of the CSV file and add to rawPlotData String[] nextLine; while ((nextLine = reader.readNext()) != null) { rawPlotData.add(nextLine); } } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "Exception reading plot file", ioe); } finally { if (reader != null) { try { reader.close(); } catch (IOException ignore) { // ignore } } } } /** * Saves the plot data to the CSV file on disk. The CSV file is stored in * the projects root directory. The data is read from the rawPlotData * instance variable. */ private void savePlotData() { File plotFile = new File(project.getRootDir(), getCsvFileName()); CSVWriter writer = null; try { writer = new CSVWriter(new FileWriter(plotFile)); // write 2 header lines String[] header1 = new String[] { Messages.Plot_Title(), this.getTitle() }; String[] header2 = new String[] { Messages.Plot_Value(), Messages.Plot_SeriesLabel(), Messages.Plot_BuildNumber(), Messages.Plot_BuildDate(), Messages.Plot_URL() }; writer.writeNext(header1); writer.writeNext(header2); // write each entry of rawPlotData to a new line in the CSV file for (String[] entry : rawPlotData) { if (reportBuild(Integer.parseInt(entry[2]))) { writer.writeNext(entry); } } } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "Exception saving plot file", ioe); } finally { if (writer != null) { try { writer.close(); } catch (IOException ignore) { // ignore } } } } /** * @return true if the build should be part of the graph. */ /* package */boolean reportBuild(int buildNumber) { int numBuilds; try { numBuilds = Integer.parseInt(this.numBuilds); } catch (NumberFormatException ex) { // Report all builds numBuilds = Integer.MAX_VALUE; } if (buildNumber < project.getNextBuildNumber() - numBuilds) return false; return keepRecords || project.getBuildByNumber(buildNumber) != null; } }