Java tutorial
/* File: $Id$ * Revision: $Revision$ * Author: $Author$ * Date: $Date$ * * The Netarchive Suite - Software to harvest and preserve websites * Copyright 2004-2012 The Royal Danish Library, the Danish State and * University Library, the National Library of France and the Austrian * National Library. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package dk.netarkivet.harvester.harvesting.monitor; import java.awt.Color; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.LinkedList; import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.NumberTickUnit; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StandardXYItemRenderer; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.Range; import org.jfree.data.xy.DefaultXYDataset; import org.jfree.data.xy.XYDataset; import org.jfree.ui.RectangleInsets; import dk.netarkivet.common.exceptions.ArgumentNotValid; import dk.netarkivet.common.exceptions.IOFailure; import dk.netarkivet.common.lifecycle.PeriodicTaskExecutor; import dk.netarkivet.common.utils.FileUtils; import dk.netarkivet.common.utils.I18n; import dk.netarkivet.common.utils.Settings; import dk.netarkivet.common.utils.StringUtils; import dk.netarkivet.common.utils.TimeUtils; import dk.netarkivet.harvester.HarvesterSettings; import dk.netarkivet.harvester.datamodel.NumberUtils; import dk.netarkivet.harvester.datamodel.RunningJobsInfoDAO; /** * This class implements a generator for an history chart of a running job. * The chart traces the progress percentage and the queued URI count over * the crawl time. * Charts are rendered in a PNG image file, generated in the webapp directory. */ class StartedJobHistoryChartGen { /** * Time units used to scale the crawl time values and generate the * chart's time axis ticks. */ protected static enum TimeAxisResolution { /** * One second. Tick step is 10s. */ second(1, 1, 10), /** * One minute. Tick step is 5m. */ minute(60, 60, 5), /** * One hour. Tick step is 1h. */ hour(60 * minute.seconds, 60 * minute.seconds, 1), /** * Twelve hours. Tick step is 2h. */ half_day(12 * 60 * minute.seconds, 60 * minute.seconds, 2), /** * One day. Tick step is 0.5d. */ day(24 * hour.seconds, 24 * hour.seconds, 0.5), /** * One week. Tick step is 1w. */ week(7 * day.seconds, 7 * day.seconds, 1); /** * The time unit in seconds. */ private final int seconds; /** * The scale in seconds. */ private final int scaleSeconds; /** * The step between two tick units. */ private final double tickStep; /** * Builds a time axis resolution. * @param seconds the actual resolution in seconds * @param scaleSeconds the actual "scale" of ticks * @param tickStep the number of ticks in one step. */ TimeAxisResolution(int seconds, int scaleSeconds, double tickStep) { this.seconds = seconds; this.scaleSeconds = scaleSeconds; this.tickStep = tickStep; } /** * Scale down an array of seconds. * @param timeInSeconds An array of seconds * @return a scaled down version of the given array of seconds */ double[] scale(double[] timeInSeconds) { double[] scaledTime = new double[timeInSeconds.length]; for (int i = 0; i < timeInSeconds.length; i++) { scaledTime[i] = timeInSeconds[i] / this.scaleSeconds; } return scaledTime; } /** * @param seconds the seconds * @return the proper timeUnit for the given argument */ static TimeAxisResolution findTimeUnit(double seconds) { TimeAxisResolution[] allTus = values(); for (int i = 0; i < allTus.length - 1; i++) { TimeAxisResolution nextGreater = allTus[i + 1]; if (seconds < nextGreater.seconds) { return allTus[i]; } } return week; // largest unit } } /** * A chart generation task. Generates a PNG image for a * job progress history. */ private static class ChartGen implements Runnable { /** The process that generates the Charts. */ private final StartedJobHistoryChartGen gen; /** * Constructor of a ChartGen objector. * @param gen the process that generates the charts. */ ChartGen(StartedJobHistoryChartGen gen) { super(); this.gen = gen; } @Override public void run() { synchronized (gen) { gen.chartFile = null; } long jobId = gen.jobId; StartedJobInfo[] fullHistory = RunningJobsInfoDAO.getInstance().getFullJobHistory(jobId); LinkedList<Double> timeValues = new LinkedList<Double>(); LinkedList<Double> progressValues = new LinkedList<Double>(); LinkedList<Double> urlValues = new LinkedList<Double>(); for (StartedJobInfo sji : fullHistory) { timeValues.add((double) sji.getElapsedSeconds()); progressValues.add(sji.getProgress()); urlValues.add((double) sji.getQueuedFilesCount()); } // Refresh the history png image for the job. File pngFile = new File(gen.outputFolder, jobId + "-history.png"); File newPngFile; try { newPngFile = File.createTempFile(jobId + "-history", "." + System.currentTimeMillis() + ".png"); } catch (IOException e) { LOG.warn("Failed to create temp PNG file for job " + jobId); return; } long startTime = System.currentTimeMillis(); gen.generatePngChart(newPngFile, CHART_RESOLUTION[0], CHART_RESOLUTION[1], null, // no chart title I18N.getString(gen.locale, "running.job.details.chart.legend.crawlTime"), new String[] { I18N.getString(gen.locale, "running.job.details.chart.legend.progress"), I18N.getString(gen.locale, "running.job.details.chart.legend.queuedUris") }, NumberUtils.toPrimitiveArray(timeValues), new double[][] { new double[] { 0, 100 }, null }, new double[][] { NumberUtils.toPrimitiveArray(progressValues), NumberUtils.toPrimitiveArray(urlValues) }, new Color[] { Color.blue, Color.green.darker() }, new String[] { "%", "" }, false, Color.lightGray.brighter().brighter()); long genTime = System.currentTimeMillis() - startTime; LOG.info("Generated history chart for job " + jobId + " in " + (genTime < TimeUtils.SECOND_IN_MILLIS ? genTime + " ms" : StringUtils.formatDuration(genTime / TimeUtils.SECOND_IN_MILLIS)) + "."); synchronized (gen) { // Overwrite old file, then delete temp file try { FileUtils.copyFile(newPngFile, pngFile); FileUtils.remove(newPngFile); } catch (IOFailure iof) { LOG.error("IOFailure while copying PNG file", iof); } gen.chartFile = pngFile; } } } /** The class logger. */ static final Log LOG = LogFactory.getLog(StartedJobHistoryChartGen.class); /** Internationalisation object. */ private static final I18n I18N = new I18n(dk.netarkivet.harvester.Constants.TRANSLATIONS_BUNDLE); /** * Rate in seconds at which history charts should be generated. */ private static final long GEN_INTERVAL = Settings .getLong(HarvesterSettings.HARVEST_MONITOR_HISTORY_CHART_GEN_INTERVAL); /** * The chart image resolution. */ private static final int[] CHART_RESOLUTION = new int[] { 600, 450 }; /** The dimension of the chart axis. */ private static final double CHART_AXIS_DIMENSION = 10.0; /** The relative path of the output. */ private static final String OUTPUT_REL_PATH = "History" + File.separator + "webapp"; /** The job id. */ private final long jobId; /** * The folder where image files are output. */ private final File outputFolder; /** * The chart image file. */ private File chartFile = null; /** * The locale for internationalizing the chart. * The locale is set to the system default. */ private final Locale locale; /** The process controlling the cyclic regeneration of charts. */ private PeriodicTaskExecutor genExec = null; /** * Constructor. Start generating charts for data belonging to the * given job. * @param jobId a job id. */ StartedJobHistoryChartGen(long jobId) { super(); this.outputFolder = new File(FileUtils.getTempDir() + File.separator + OUTPUT_REL_PATH); this.jobId = jobId; // Set the locale to the system default this.locale = Locale.getDefault(); genExec = new PeriodicTaskExecutor("ChartGen", new ChartGen(this), 0, GEN_INTERVAL); } /** * Returns the image file. * @return the image file. Might return null if no file is currently * available. */ public synchronized File getChartFile() { return chartFile; } /** * Deletes the chart image if it exists and stops the generation schedule. */ public void cleanup() { if (chartFile != null && chartFile.exists()) { if (!chartFile.delete()) { chartFile.deleteOnExit(); } } if (genExec != null) { genExec.shutdown(); } } /** * Generates a chart in PNG format. * @param outputFile the output file, it should exist. * @param pxWidth the image width in pixels. * @param pxHeight the image height in pixels. * @param chartTitle the chart title, may be null. * @param xAxisTitle the x axis title * @param yDataSeriesRange the axis range (null for auto) * @param yDataSeriesTitles the Y axis titles. * @param timeValuesInSeconds the time values in seconds * @param yDataSeries the Y axis value series. * @param yDataSeriesColors the Y axis value series drawing colors. * @param yDataSeriesTickSuffix TODO explain argument yDataSeriesTickSuffix * @param drawBorder draw, or not, the border. * @param backgroundColor the chart background color. */ final void generatePngChart(File outputFile, int pxWidth, int pxHeight, String chartTitle, String xAxisTitle, String[] yDataSeriesTitles, double[] timeValuesInSeconds, double[][] yDataSeriesRange, double[][] yDataSeries, Color[] yDataSeriesColors, String[] yDataSeriesTickSuffix, boolean drawBorder, Color backgroundColor) { // Domain axis NumberAxis xAxis = new NumberAxis(xAxisTitle); xAxis.setFixedDimension(CHART_AXIS_DIMENSION); xAxis.setLabelPaint(Color.black); xAxis.setTickLabelPaint(Color.black); double maxSeconds = getMaxValue(timeValuesInSeconds); TimeAxisResolution xAxisRes = TimeAxisResolution.findTimeUnit(maxSeconds); xAxis.setTickUnit(new NumberTickUnit(xAxisRes.tickStep)); double[] scaledTimeValues = xAxisRes.scale(timeValuesInSeconds); String tickSymbol = I18N.getString(locale, "running.job.details.chart.timeunit.symbol." + xAxisRes.name()); xAxis.setNumberFormatOverride(new DecimalFormat("###.##'" + tickSymbol + "'")); // First dataset String firstDataSetTitle = yDataSeriesTitles[0]; XYDataset firstDataSet = createXYDataSet(firstDataSetTitle, scaledTimeValues, yDataSeries[0]); Color firstDataSetColor = yDataSeriesColors[0]; // First range axis NumberAxis firstYAxis = new NumberAxis(firstDataSetTitle); firstYAxis.setFixedDimension(CHART_AXIS_DIMENSION); setAxisRange(firstYAxis, yDataSeriesRange[0]); firstYAxis.setLabelPaint(firstDataSetColor); firstYAxis.setTickLabelPaint(firstDataSetColor); String firstAxisTickSuffix = yDataSeriesTickSuffix[0]; if (firstAxisTickSuffix != null && !firstAxisTickSuffix.isEmpty()) { firstYAxis.setNumberFormatOverride(new DecimalFormat("###.##'" + firstAxisTickSuffix + "'")); } // Create the plot with domain axis and first range axis XYPlot plot = new XYPlot(firstDataSet, xAxis, firstYAxis, null); XYLineAndShapeRenderer firstRenderer = new XYLineAndShapeRenderer(true, false); plot.setRenderer(firstRenderer); plot.setOrientation(PlotOrientation.VERTICAL); plot.setBackgroundPaint(Color.lightGray); plot.setDomainGridlinePaint(Color.white); plot.setRangeGridlinePaint(Color.white); plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0)); firstRenderer.setSeriesPaint(0, firstDataSetColor); // Now iterate on next axes for (int i = 1; i < yDataSeries.length; i++) { // Create axis String seriesTitle = yDataSeriesTitles[i]; Color seriesColor = yDataSeriesColors[i]; NumberAxis yAxis = new NumberAxis(seriesTitle); yAxis.setFixedDimension(CHART_AXIS_DIMENSION); setAxisRange(yAxis, yDataSeriesRange[i]); yAxis.setLabelPaint(seriesColor); yAxis.setTickLabelPaint(seriesColor); String yAxisTickSuffix = yDataSeriesTickSuffix[i]; if (yAxisTickSuffix != null && !yAxisTickSuffix.isEmpty()) { yAxis.setNumberFormatOverride(new DecimalFormat("###.##'" + yAxisTickSuffix + "'")); } // Create dataset and add axis to plot plot.setRangeAxis(i, yAxis); plot.setRangeAxisLocation(i, AxisLocation.BOTTOM_OR_LEFT); plot.setDataset(i, createXYDataSet(seriesTitle, scaledTimeValues, yDataSeries[i])); plot.mapDatasetToRangeAxis(i, i); XYItemRenderer renderer = new StandardXYItemRenderer(); renderer.setSeriesPaint(0, seriesColor); plot.setRenderer(i, renderer); } // Create the chart JFreeChart chart = new JFreeChart(chartTitle, JFreeChart.DEFAULT_TITLE_FONT, plot, false); // Customize rendering chart.setBackgroundPaint(Color.white); chart.setBorderVisible(true); chart.setBorderPaint(Color.BLACK); // Render image try { ChartUtilities.saveChartAsPNG(outputFile, chart, pxWidth, pxHeight); } catch (IOException e) { LOG.error("Chart export failed", e); } } /** * Create a XYDataset based on the given arguments. * @param name The name * @param timeValues The timevalues * @param values the values * @return a DefaultXYDataset. */ private XYDataset createXYDataSet(String name, double[] timeValues, double[] values) { DefaultXYDataset ds = new DefaultXYDataset(); ds.addSeries(name, new double[][] { timeValues, values }); return ds; } /** * Find the maximum of the values given. If this maximum is less than * {@link Double#MIN_VALUE} then {@link Double#MIN_VALUE} is returned. * @param values an array of doubles * @return the maximum of the values given */ private double getMaxValue(double[] values) { double max = Double.MIN_VALUE; for (double v : values) { max = Math.max(v, max); } return max; } /** * Set the axis range. * @param axis a numberAxis * @param range a range */ private void setAxisRange(NumberAxis axis, double[] range) { if (range == null || range.length != 2) { axis.setAutoRange(true); } else { double lower = range[0]; double upper = range[1]; ArgumentNotValid.checkTrue(lower < upper, "Incorrect range"); axis.setAutoRange(false); axis.setRange(new Range(lower, upper)); } } }