com.slamd.report.PDFReportGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.slamd.report.PDFReportGenerator.java

Source

/*
 *                             Sun Public License
 *
 * The contents of this file are subject to the Sun Public License Version
 * 1.0 (the "License").  You may not use this file except in compliance with
 * the License.  A copy of the License is available at http://www.sun.com/
 *
 * The Original Code is the SLAMD Distributed Load Generation Engine.
 * The Initial Developer of the Original Code is Neil A. Wilson.
 * Portions created by Neil A. Wilson are Copyright (C) 2004-2019.
 * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc.
 * All Rights Reserved.
 *
 * Contributor(s):  Neil A. Wilson
 */
package com.slamd.report;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.servlet.http.HttpServletResponse;

import com.sun.media.jai.codec.ImageCodec;
import com.sun.media.jai.codec.ImageEncoder;

import com.lowagie.text.Anchor;
import com.lowagie.text.Cell;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfPageEvent;
import com.lowagie.text.pdf.PdfWriter;

import com.slamd.admin.RequestInfo;
import com.slamd.common.Constants;
import com.slamd.common.DynamicConstants;
import com.slamd.job.Job;
import com.slamd.job.OptimizationAlgorithm;
import com.slamd.job.OptimizingJob;
import com.slamd.parameter.BooleanParameter;
import com.slamd.parameter.Parameter;
import com.slamd.parameter.ParameterList;
import com.slamd.stat.StatTracker;

/**
 * This class provides an implementation of a SLAMD report generator that will
 * write the report information to a PDF document.
 *
 *
 * @author   Neil A. Wilson
 */
public class PDFReportGenerator implements ReportGenerator, PdfPageEvent {
    /**
     * The name of the configuration parameter that indicates whether to include
     * graphs in the report.
     */
    public static final String PARAM_INCLUDE_GRAPHS = "include_graphs";

    /**
     * The name of the configuration parameter that indicates whether to include
     * resource monitor statistics in the report.
     */
    public static final String PARAM_INCLUDE_MONITOR_STATS = "include_monitor_config";

    /**
     * The name of the configuration parameter that indicates whether to include
     * the job-specific configuration in the report.
     */
    public static final String PARAM_INCLUDE_JOB_CONFIG = "include_job_config";

    /**
     * The name of the configuration parameter that indicates whether to include
     * the schedule configuration in the report.
     */
    public static final String PARAM_INCLUDE_SCHEDULE_CONFIG = "include_schedule_config";

    /**
     * The name of the configuration parameter that indicates whether to include
     * job statistics in the report.
     */
    public static final String PARAM_INCLUDE_STATS = "include_stats";

    /**
     * The name of the configuration parameter that indicates whether to only
     * include jobs that contain statistics in the generated report.
     */
    public static final String PARAM_REQUIRE_STATS = "require_stats";

    /**
     * The name of the configuration parameter that indicates whether to
     * include the individual iterations of an optimizing job.
     */
    public static final String PARAM_INCLUDE_OPTIMIZING_ITERATIONS = "include_optimizing_iterations";

    /**
     * The name of the configuration parameter that indicates whether to view the
     * resulting PDF document in the browser (if supported) or save to disk.
     */
    public static final String PARAM_VIEW_IN_BROWSER = "view_in_browser";

    // The list used to hold the jobs that will be included in the generated
    // report.
    private ArrayList<Job> jobList;

    // The list used to hold the optimizing jobs that will be included in the
    // generated report.
    private ArrayList<OptimizingJob> optimizingJobList;

    // Indicates whether to include graphs in the generated results.
    private boolean includeGraphs;

    // Indicates whether to include the job-specific configuration in the
    // generated report.
    private boolean includeJobConfig;

    // Indicates whether to include resource monitor statistics in the generated
    // report.
    private boolean includeMonitorStats;

    // Indicates whether to include the schedule configuration in the generated
    // report.
    private boolean includeScheduleConfig;

    // Indicates whether to include job statistics in the generated report.
    private boolean includeStats;

    // Indicates whether to only include jobs that have statistics.
    private boolean requireStats;

    // Indicates whether to include information for individual iterations of an
    // optimizing job.
    private boolean includeOptimizingIterations;

    // Indicates whether to view the resulting PDF in a browser or save it to
    // disk.
    private boolean viewInBrowser;

    // The decimal format that will be used to format floating-point values.
    private DecimalFormat decimalFormat;

    // The set of jobs that will actually be included in the report.
    private Job[] reportJobs;

    // The set of optimizing jobs that will actually be included in the report.
    private OptimizingJob[] reportOptimizingJobs;

    // The date formatter that will be used when writing out dates.
    private SimpleDateFormat dateFormat;

    /**
     * Creates a new text report generator.
     */
    public PDFReportGenerator() {
        jobList = new ArrayList<Job>();
        optimizingJobList = new ArrayList<OptimizingJob>();
        dateFormat = new SimpleDateFormat(Constants.DISPLAY_DATE_FORMAT);
        decimalFormat = new DecimalFormat("0.000");

        includeScheduleConfig = true;
        includeJobConfig = true;
        includeStats = true;
        includeMonitorStats = true;
        includeGraphs = true;
        requireStats = true;
        includeOptimizingIterations = true;
        viewInBrowser = false;
    }

    /**
     * Retrieves a user-friendly name that can be used to indicate the type of
     * report that will be generated.
     *
     * @return  The user-friendly name that can be used to indicate the type of
     *          report that will be generated.
     */
    public String getReportGeneratorName() {
        return "PDF Report Generator";
    }

    /**
     * Retrieves a new instance of this report generator initialized with the
     * default configuration.
     *
     * @return  A new instance of this report generator initialized with the
     *          default configuration.
     */
    public ReportGenerator newInstance() {
        return new PDFReportGenerator();
    }

    /**
     * Retrieves a set of parameters that can be used to allow the user to
     * configure the way that the report is generated.
     *
     * @return  A set of parameters that can be used to allow the user to
     *          configure the way that the report is generated.
     */
    public ParameterList getReportParameterStubs() {
        BooleanParameter includeScheduleConfigParameter = new BooleanParameter(PARAM_INCLUDE_SCHEDULE_CONFIG,
                "Include Schedule Configuration",
                "Indicates whether the schedule configuration " + "information should be included in the report.",
                includeScheduleConfig);
        BooleanParameter includeJobConfigParameter = new BooleanParameter(PARAM_INCLUDE_JOB_CONFIG,
                "Include Job-Specific Configuration", "Indicates whether the job-specific "
                        + "configuration information should be included " + "in the report.",
                includeJobConfig);
        BooleanParameter includeStatsParameter = new BooleanParameter(PARAM_INCLUDE_STATS, "Include Job Statistics",
                "Indicates whether the statistics collected " + "from job execution should be included in the "
                        + "report.",
                includeStats);
        BooleanParameter includeMonitorParameter = new BooleanParameter(PARAM_INCLUDE_MONITOR_STATS,
                "Include Resource Monitor Statistics", "Indicates whether the statistics collected "
                        + "from job execution should be included in the " + "report.",
                includeMonitorStats);
        BooleanParameter includeGraphsParameter = new BooleanParameter(PARAM_INCLUDE_GRAPHS,
                "Include Graphs of Statistics",
                "Indicates whether graphs of statistical " + "information should be included in the report.",
                includeGraphs);
        BooleanParameter requireStatsParameter = new BooleanParameter(PARAM_REQUIRE_STATS,
                "Only Include Jobs with Statistics",
                "Indicates whether to only include jobs that " + "have statistics available.", requireStats);
        BooleanParameter includeOptimizingParameter = new BooleanParameter(PARAM_INCLUDE_OPTIMIZING_ITERATIONS,
                "Include Optimizing Job Iterations",
                "Indicates whether to include data for  " + "optimizing job iterations.",
                includeOptimizingIterations);
        BooleanParameter viewInBrowserParameter = new BooleanParameter(PARAM_VIEW_IN_BROWSER, "View PDF in Browser",
                "Indicates whether the resulting PDF file " + "should be viewed in the browser or save it to "
                        + "disk.",
                viewInBrowser);

        Parameter[] parameters = new Parameter[] { includeScheduleConfigParameter, includeJobConfigParameter,
                includeStatsParameter, includeMonitorParameter, includeGraphsParameter, requireStatsParameter,
                includeOptimizingParameter, viewInBrowserParameter };

        return new ParameterList(parameters);
    }

    /**
     * Initializes this reporter based on the parameters customized by the end
     * user.
     *
     * @param  reportParameters  The set of parameters provided by the end user
     *                           that should be used to customize the report.
     */
    public void initializeReporter(ParameterList reportParameters) {
        BooleanParameter bp = reportParameters.getBooleanParameter(PARAM_INCLUDE_SCHEDULE_CONFIG);
        if (bp != null) {
            includeScheduleConfig = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_INCLUDE_JOB_CONFIG);
        if (bp != null) {
            includeJobConfig = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_INCLUDE_STATS);
        if (bp != null) {
            includeStats = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_INCLUDE_MONITOR_STATS);
        if (bp != null) {
            includeMonitorStats = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_INCLUDE_GRAPHS);
        if (bp != null) {
            includeGraphs = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_REQUIRE_STATS);
        if (bp != null) {
            requireStats = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_INCLUDE_OPTIMIZING_ITERATIONS);
        if (bp != null) {
            includeOptimizingIterations = bp.getBooleanValue();
        }

        bp = reportParameters.getBooleanParameter(PARAM_VIEW_IN_BROWSER);
        if (bp != null) {
            viewInBrowser = bp.getBooleanValue();
        }
    }

    /**
     * Indicates that information about the provided job should be included in the
     * report.
     *
     * @param  job  The job about which to include information in the report.
     */
    public void addJobReport(Job job) {
        if (requireStats && (!job.hasStats())) {
            return;
        }

        jobList.add(job);
    }

    /**
     * Indicates that information about the provided optimizing job should be
     * included in the report.
     *
     * @param  optimizingJob  The optimizing job about which to include
     *                        information in the report.
     */
    public void addOptimizingJobReport(OptimizingJob optimizingJob) {
        if (requireStats && (!optimizingJob.hasStats())) {
            return;
        }

        optimizingJobList.add(optimizingJob);
    }

    /**
     * Generates the report and sends it to the user over the provided servlet
     * response.
     *
     * @param  requestInfo  State information about the request being processed.
     */
    public void generateReport(RequestInfo requestInfo) {
        // Determine exactly what to include in the report.  We will want to strip
        // out any individual jobs that are part of an optimizing job that is also
        // to be included in the report.
        reportOptimizingJobs = new OptimizingJob[optimizingJobList.size()];
        optimizingJobList.toArray(reportOptimizingJobs);

        ArrayList<Job> tmpList = new ArrayList<Job>(jobList.size());
        for (int i = 0; i < jobList.size(); i++) {
            Job job = jobList.get(i);
            String optimizingJobID = job.getOptimizingJobID();
            if ((optimizingJobID != null) && (optimizingJobID.length() > 0)) {
                boolean matchFound = false;

                for (int j = 0; j < reportOptimizingJobs.length; j++) {
                    if (optimizingJobID.equalsIgnoreCase(reportOptimizingJobs[j].getOptimizingJobID())) {
                        matchFound = true;
                        break;
                    }
                }

                if (matchFound) {
                    continue;
                }
            }

            tmpList.add(job);
        }
        reportJobs = new Job[tmpList.size()];
        tmpList.toArray(reportJobs);

        // Prepare to actually generate the report and send it to the user.
        HttpServletResponse response = requestInfo.getResponse();
        if (viewInBrowser) {
            response.setContentType("application/pdf");
        } else {
            response.setContentType("application/x-slamd-report-pdf");
        }
        response.addHeader("Content-Disposition", "filename=\"slamd_data_report.pdf\"");

        try {
            // Create the PDF document and associate it with the response to send to
            // the client.
            Document document = new Document(PageSize.LETTER);
            PdfWriter writer = PdfWriter.getInstance(document, response.getOutputStream());
            document.addTitle("SLAMD Generated Report");
            document.addCreationDate();
            document.addCreator("SLAMD Distributed Load Generator");
            writer.setPageEvent(this);

            // Open the document and add the table of contents.
            document.open();

            boolean needNewPage = writeContents(document);

            // Write the regular job information.
            for (int i = 0; i < reportJobs.length; i++) {
                if (needNewPage) {
                    document.newPage();
                }
                writeJob(document, reportJobs[i]);
                needNewPage = true;
            }

            // Write the optimizing job information.
            for (int i = 0; i < reportOptimizingJobs.length; i++) {
                if (needNewPage) {
                    document.newPage();
                }
                writeOptimizingJob(document, reportOptimizingJobs[i]);
                needNewPage = true;
            }

            // Close the document.
            document.close();
        } catch (Exception e) {
            // Not much we can do about this.
            e.printStackTrace();
            return;
        }
    }

    /**
     * Writes the table of contents to the document.
     *
     * @param  document  The document to which the contents are to be written.
     *
     * @return  {@code true} if the contents information was written to the
     *          PDF document, or {@code false} if not.
     *
     * @throws  DocumentException  If a problem occurs while writing the contents.
     */
    private boolean writeContents(Document document) throws DocumentException {
        // First, make sure that there is actually something to write.  If we're
        // only going to write information for a single job or optimizing job, then
        // there is no reason to have a contents section.
        if (((reportJobs.length == 1) && (reportOptimizingJobs.length == 0))
                || ((reportJobs.length == 0) && (reportOptimizingJobs.length == 1))) {
            return false;
        }

        if (reportJobs.length > 0) {
            // Write the job data header.
            Paragraph p = new Paragraph("Job Data",
                    FontFactory.getFont(FontFactory.HELVETICA, 18, Font.BOLD, Color.BLACK));
            document.add(p);

            // Create a table with the list of jobs.
            PdfPTable table = new PdfPTable(3);
            table.setWidthPercentage(100);
            writeTableHeaderCell(table, "Job ID");
            writeTableHeaderCell(table, "Description");
            writeTableHeaderCell(table, "Job Type");

            for (int i = 0; i < reportJobs.length; i++) {
                Job job = reportJobs[i];
                Anchor anchor = new Anchor(job.getJobID(),
                        FontFactory.getFont(FontFactory.HELVETICA, 12, Font.UNDERLINE, Color.BLUE));
                anchor.setReference('#' + job.getJobID());
                table.addCell(new PdfPCell(anchor));

                String descriptionStr = job.getJobDescription();
                if ((descriptionStr == null) || (descriptionStr.length() == 0)) {
                    descriptionStr = "(Not Specified)";
                }
                writeTableCell(table, descriptionStr);

                writeTableCell(table, job.getJobClass().getJobName());
            }
            document.add(table);

            // Write a blank line between the job data and optimizing job data.
            document.add(new Paragraph(" "));
        }

        if (reportOptimizingJobs.length > 0) {
            // Write the optimizing job data header.
            Paragraph p = new Paragraph("Optimizing Job Data",
                    FontFactory.getFont(FontFactory.HELVETICA, 18, Font.BOLD, Color.BLACK));
            document.add(p);

            // Create a table with the list of jobs.
            PdfPTable table = new PdfPTable(3);
            table.setWidthPercentage(100);
            writeTableHeaderCell(table, "Optimizing Job ID");
            writeTableHeaderCell(table, "Description");
            writeTableHeaderCell(table, "Job Type");

            for (int i = 0; i < reportOptimizingJobs.length; i++) {
                OptimizingJob optimizingJob = reportOptimizingJobs[i];
                Anchor anchor = new Anchor(optimizingJob.getOptimizingJobID(),
                        FontFactory.getFont(FontFactory.HELVETICA, 12, Font.UNDERLINE, Color.BLUE));
                anchor.setReference('#' + optimizingJob.getOptimizingJobID());
                table.addCell(new PdfPCell(anchor));

                String descriptionStr = optimizingJob.getDescription();
                if ((descriptionStr == null) || (descriptionStr.length() == 0)) {
                    descriptionStr = "(Not Specified)";
                }
                writeTableCell(table, descriptionStr);

                writeTableCell(table, optimizingJob.getJobClass().getJobName());
            }
            document.add(table);
        }

        return true;
    }

    /**
     * Writes information about the provided job to the document.
     *
     * @param  document  The document to which the job information should be
     *                   written.
     * @param  job       The job to include in the document.
     *
     * @throws  DocumentException  If a problem occurs while writing the contents.
     */
    private void writeJob(Document document, Job job) throws DocumentException {
        Anchor anchor = new Anchor("Job " + job.getJobID(),
                FontFactory.getFont(FontFactory.HELVETICA, 18, Font.BOLD, Color.BLACK));
        anchor.setName(job.getJobID());
        Paragraph p = new Paragraph(anchor);
        document.add(p);

        // Write the general information to the document.
        p = new Paragraph("General Information",
                FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
        document.add(p);

        PdfPTable table = new PdfPTable(2);
        table.setWidthPercentage(100);
        table.setWidths(new int[] { 30, 70 });
        writeTableCell(table, "Job ID");
        writeTableCell(table, job.getJobID());

        String optimizingJobID = job.getOptimizingJobID();
        if ((optimizingJobID != null) && (optimizingJobID.length() > 0)) {
            writeTableCell(table, "Optimizing Job ID");
            writeTableCell(table, optimizingJobID);
        }

        String descriptionStr = job.getJobDescription();
        if ((descriptionStr == null) || (descriptionStr.length() == 0)) {
            descriptionStr = "(Not Specified)";
        }
        writeTableCell(table, "Job Description");
        writeTableCell(table, descriptionStr);

        writeTableCell(table, "Job Type");
        writeTableCell(table, job.getJobClassName());

        writeTableCell(table, "Job Class");
        writeTableCell(table, job.getJobClass().getClass().getName());

        writeTableCell(table, "Job State");
        writeTableCell(table, job.getJobStateString());
        document.add(table);

        // Write the schedule config if appropriate.
        if (includeScheduleConfig) {
            document.add(new Paragraph(" "));
            p = new Paragraph("Schedule Information",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            Date startTime = job.getStartTime();
            String startStr;
            if (startTime == null) {
                startStr = "(Not Available)";
            } else {
                startStr = dateFormat.format(startTime);
            }
            writeTableCell(table, "Scheduled Start Time");
            writeTableCell(table, startStr);

            Date stopTime = job.getStopTime();
            String stopStr;
            if (stopTime == null) {
                stopStr = "(Not Specified)";
            } else {
                stopStr = dateFormat.format(stopTime);
            }
            writeTableCell(table, "Scheduled Stop Time");
            writeTableCell(table, stopStr);

            int duration = job.getDuration();
            String durationStr;
            if (duration > 0) {
                durationStr = duration + " seconds";
            } else {
                durationStr = "(Not Specified)";
            }
            writeTableCell(table, "Scheduled Duration");
            writeTableCell(table, durationStr);

            writeTableCell(table, "Number of Clients");
            writeTableCell(table, String.valueOf(job.getNumberOfClients()));

            String[] requestedClients = job.getRequestedClients();
            if ((requestedClients != null) && (requestedClients.length > 0)) {
                PdfPTable clientTable = new PdfPTable(1);
                for (int i = 0; i < requestedClients.length; i++) {
                    PdfPCell clientCell = new PdfPCell(new Phrase(requestedClients[i]));
                    clientCell.setBorder(0);
                    clientTable.addCell(clientCell);
                }

                writeTableCell(table, "Requested Clients");
                table.addCell(clientTable);
            }

            String[] monitorClients = job.getResourceMonitorClients();
            if ((monitorClients != null) && (monitorClients.length > 0)) {
                PdfPTable clientTable = new PdfPTable(1);
                for (int i = 0; i < monitorClients.length; i++) {
                    PdfPCell clientCell = new PdfPCell(new Phrase(monitorClients[i]));
                    clientCell.setBorder(0);
                    clientTable.addCell(clientCell);
                }

                writeTableCell(table, "Resource Monitor Clients");
                table.addCell(clientTable);
            }

            writeTableCell(table, "Threads per Client");
            writeTableCell(table, String.valueOf(job.getThreadsPerClient()));

            writeTableCell(table, "Thread Startup Delay");
            writeTableCell(table, job.getThreadStartupDelay() + " milliseconds");

            writeTableCell(table, "Statistics Collection Interval");
            writeTableCell(table, job.getCollectionInterval() + " seconds");

            document.add(table);
        }

        // Write the job-specific parameter information if appropriate.
        if (includeJobConfig) {
            document.add(new Paragraph(" "));
            p = new Paragraph("Parameter Information",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            Parameter[] params = job.getParameterList().getParameters();
            for (int i = 0; i < params.length; i++) {
                writeTableCell(table, params[i].getDisplayName());
                writeTableCell(table, params[i].getDisplayValue());
            }

            document.add(table);
        }

        // Write the statistical data if appropriate.
        if (includeStats && job.hasStats()) {
            document.add(new Paragraph(" "));
            p = new Paragraph("General Execution Data",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            Date actualStartTime = job.getActualStartTime();
            String startStr;
            if (actualStartTime == null) {
                startStr = "(Not Available)";
            } else {
                startStr = dateFormat.format(actualStartTime);
            }
            writeTableCell(table, "Actual Start Time");
            writeTableCell(table, startStr);

            Date actualStopTime = job.getActualStopTime();
            String stopStr;
            if (actualStopTime == null) {
                stopStr = "(Not Available)";
            } else {
                stopStr = dateFormat.format(actualStopTime);
            }
            writeTableCell(table, "Actual Stop Time");
            writeTableCell(table, stopStr);

            int actualDuration = job.getActualDuration();
            String durationStr;
            if (actualDuration > 0) {
                durationStr = actualDuration + " seconds";
            } else {
                durationStr = "(Not Available)";
            }
            writeTableCell(table, "Actual Duration");
            writeTableCell(table, durationStr);

            String[] clients = job.getStatTrackerClientIDs();
            if ((clients != null) && (clients.length > 0)) {
                PdfPTable clientTable = new PdfPTable(1);
                for (int i = 0; i < clients.length; i++) {
                    PdfPCell clientCell = new PdfPCell(new Phrase(clients[i]));
                    clientCell.setBorder(0);
                    clientTable.addCell(clientCell);
                }

                writeTableCell(table, "Clients Used");
                table.addCell(clientTable);
            }

            document.add(table);

            String[] trackerNames = job.getStatTrackerNames();
            for (int i = 0; i < trackerNames.length; i++) {
                StatTracker[] trackers = job.getStatTrackers(trackerNames[i]);
                if ((trackers != null) && (trackers.length > 0)) {
                    document.newPage();
                    StatTracker tracker = trackers[0].newInstance();
                    tracker.aggregate(trackers);

                    document.add(new Paragraph(" "));
                    document.add(new Paragraph(trackerNames[i],
                            FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK)));

                    String[] summaryNames = tracker.getSummaryLabels();
                    String[] summaryValues = tracker.getSummaryData();
                    table = new PdfPTable(2);
                    table.setWidthPercentage(100);
                    table.setWidths(new int[] { 50, 50 });
                    for (int j = 0; j < summaryNames.length; j++) {
                        writeTableCell(table, summaryNames[j]);
                        writeTableCell(table, summaryValues[j]);
                    }
                    document.add(table);

                    if (includeGraphs) {
                        try {
                            ParameterList params = tracker.getGraphParameterStubs(job);
                            BufferedImage graphImage = tracker.createGraph(job, Constants.DEFAULT_GRAPH_WIDTH,
                                    Constants.DEFAULT_GRAPH_HEIGHT, params);
                            Image image = Image.getInstance(imageToByteArray(graphImage));
                            image.scaleToFit(inchesToPoints(5.5), inchesToPoints(4.5));
                            document.add(image);
                        } catch (Exception e) {
                        }
                    }
                }
            }
        }

        // Write the resource monitor data if appropriate.
        if (includeMonitorStats && job.hasResourceStats()) {
            String[] trackerNames = job.getResourceStatTrackerNames();
            for (int i = 0; i < trackerNames.length; i++) {
                StatTracker[] trackers = job.getResourceStatTrackers(trackerNames[i]);
                if ((trackers != null) && (trackers.length > 0)) {
                    document.newPage();
                    StatTracker tracker = trackers[0].newInstance();
                    tracker.aggregate(trackers);

                    document.add(new Paragraph(" "));
                    document.add(new Paragraph(trackerNames[i],
                            FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK)));

                    String[] summaryNames = tracker.getSummaryLabels();
                    String[] summaryValues = tracker.getSummaryData();
                    table = new PdfPTable(2);
                    table.setWidthPercentage(100);
                    table.setWidths(new int[] { 50, 50 });
                    for (int j = 0; j < summaryNames.length; j++) {
                        writeTableCell(table, summaryNames[j]);
                        writeTableCell(table, summaryValues[j]);
                    }
                    document.add(table);

                    if (includeGraphs) {
                        try {
                            ParameterList params = tracker.getGraphParameterStubs(job);
                            BufferedImage graphImage = tracker.createMonitorGraph(job,
                                    Constants.DEFAULT_GRAPH_WIDTH, Constants.DEFAULT_MONITOR_GRAPH_HEIGHT, params);
                            Image image = Image.getInstance(imageToByteArray(graphImage));
                            image.scaleToFit(inchesToPoints(5.5), inchesToPoints(4.5));
                            document.add(image);
                        } catch (Exception e) {
                        }
                    }
                }
            }
        }
    }

    /**
     * Writes information about the provided optimizing job to the document.
     *
     * @param  document       The document to which the job information should be
     *                        written.
     * @param  optimizingJob  The optimizing job to include in the document.
     *
     * @throws  DocumentException  If a problem occurs while writing the contents.
     */
    private void writeOptimizingJob(Document document, OptimizingJob optimizingJob) throws DocumentException {
        Anchor anchor = new Anchor("Optimizing Job " + optimizingJob.getOptimizingJobID(),
                FontFactory.getFont(FontFactory.HELVETICA, 18, Font.BOLD, Color.BLACK));
        anchor.setName(optimizingJob.getOptimizingJobID());
        Paragraph p = new Paragraph(anchor);
        document.add(p);

        // Write the general information to the document.
        p = new Paragraph("General Information",
                FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
        document.add(p);

        PdfPTable table = new PdfPTable(2);
        table.setWidthPercentage(100);
        table.setWidths(new int[] { 30, 70 });
        writeTableCell(table, "Optimizing Job ID");
        writeTableCell(table, optimizingJob.getOptimizingJobID());

        writeTableCell(table, "Job Type");
        writeTableCell(table, optimizingJob.getJobClassName());

        String descriptionStr = optimizingJob.getDescription();
        if ((descriptionStr == null) || (descriptionStr.length() == 0)) {
            descriptionStr = "(Not Specified)";
        }
        writeTableCell(table, "Base Description");
        writeTableCell(table, descriptionStr);

        writeTableCell(table, "Include Thread Count in Description");
        writeTableCell(table, String.valueOf(optimizingJob.includeThreadsInDescription()));

        writeTableCell(table, "Job State");
        writeTableCell(table, Constants.jobStateToString(optimizingJob.getJobState()));
        document.add(table);

        // Write the schedule config to the document if appropriate.
        if (includeScheduleConfig) {
            document.add(new Paragraph(" "));
            p = new Paragraph("Schedule Information",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            Date startTime = optimizingJob.getStartTime();
            String startStr;
            if (startTime == null) {
                startStr = "(Not Available)";
            } else {
                startStr = dateFormat.format(startTime);
            }
            writeTableCell(table, "Scheduled Start Time");
            writeTableCell(table, startStr);

            int duration = optimizingJob.getDuration();
            String durationStr;
            if (duration > 0) {
                durationStr = duration + " seconds";
            } else {
                durationStr = "(Not Specified)";
            }
            writeTableCell(table, "Job Duration");
            writeTableCell(table, durationStr);

            writeTableCell(table, "Delay Between Iterations");
            writeTableCell(table, optimizingJob.getDelayBetweenIterations() + " seconds");

            writeTableCell(table, "Number of Clients");
            writeTableCell(table, String.valueOf(optimizingJob.getNumClients()));

            String[] requestedClients = optimizingJob.getRequestedClients();
            if ((requestedClients != null) && (requestedClients.length > 0)) {
                PdfPTable clientTable = new PdfPTable(1);
                for (int i = 0; i < requestedClients.length; i++) {
                    PdfPCell clientCell = new PdfPCell(new Phrase(requestedClients[i]));
                    clientCell.setBorder(0);
                    clientTable.addCell(clientCell);
                }

                writeTableCell(table, "Requested Clients");
                table.addCell(clientTable);
            }

            String[] monitorClients = optimizingJob.getResourceMonitorClients();
            if ((monitorClients != null) && (monitorClients.length > 0)) {
                PdfPTable clientTable = new PdfPTable(1);
                for (int i = 0; i < monitorClients.length; i++) {
                    PdfPCell clientCell = new PdfPCell(new Phrase(monitorClients[i]));
                    clientCell.setBorder(0);
                    clientTable.addCell(clientCell);
                }

                writeTableCell(table, "Resource Monitor Clients");
                table.addCell(clientTable);
            }

            writeTableCell(table, "Minimum Number of Threads");
            writeTableCell(table, String.valueOf(optimizingJob.getMinThreads()));

            int maxThreads = optimizingJob.getMaxThreads();
            String maxThreadsStr;
            if (maxThreads > 0) {
                maxThreadsStr = String.valueOf(maxThreads);
            } else {
                maxThreadsStr = "(Not Specified)";
            }
            writeTableCell(table, "Maximum Number of Threads");
            writeTableCell(table, maxThreadsStr);

            writeTableCell(table, "Thread Increment Between Iterations");
            writeTableCell(table, String.valueOf(optimizingJob.getThreadIncrement()));

            writeTableCell(table, "Statistics Collection Interval");
            writeTableCell(table, optimizingJob.getCollectionInterval() + " seconds");
            document.add(table);
        }

        // Get the optimization algorithm used.
        OptimizationAlgorithm optimizationAlgorithm = optimizingJob.getOptimizationAlgorithm();
        ParameterList paramList = optimizationAlgorithm.getOptimizationAlgorithmParameters();
        Parameter[] optimizationParams = paramList.getParameters();

        // Write the optimizing config to the document if appropriate.
        if (includeScheduleConfig) {
            document.add(new Paragraph(" "));
            p = new Paragraph("Optimization Settings",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            for (int i = 0; i < optimizationParams.length; i++) {
                writeTableCell(table, optimizationParams[i].getDisplayName());
                writeTableCell(table, optimizationParams[i].getDisplayValue());
            }

            writeTableCell(table, "Maximum Consecutive Non-Improving Iterations");
            writeTableCell(table, String.valueOf(optimizingJob.getMaxNonImproving()));

            writeTableCell(table, "Re-Run Best Iteration");
            writeTableCell(table, String.valueOf(optimizingJob.reRunBestIteration()));

            int reRunDuration = optimizingJob.getReRunDuration();
            String durationStr;
            if (reRunDuration > 0) {
                durationStr = reRunDuration + " seconds";
            } else {
                durationStr = "(Not Specified)";
            }
            writeTableCell(table, "Re-Run Duration");
            writeTableCell(table, durationStr);

            document.add(table);
        }

        // Write the job-specific config to the document if appropriate.
        if (includeJobConfig) {
            document.add(new Paragraph(" "));
            p = new Paragraph("Parameter Information",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            Parameter[] params = optimizingJob.getParameters().getParameters();
            for (int i = 0; i < params.length; i++) {
                writeTableCell(table, params[i].getDisplayName());
                writeTableCell(table, params[i].getDisplayValue());
            }

            document.add(table);
        }

        // Write the statistical data to the document if appropriate.
        if (includeStats && optimizingJob.hasStats()) {
            document.add(new Paragraph(" "));
            p = new Paragraph("Execution Data",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
            document.add(p);

            table = new PdfPTable(2);
            table.setWidthPercentage(100);
            table.setWidths(new int[] { 30, 70 });

            Date actualStartTime = optimizingJob.getActualStartTime();
            String startTimeStr;
            if (actualStartTime == null) {
                startTimeStr = "(Not Available)";
            } else {
                startTimeStr = dateFormat.format(actualStartTime);
            }
            writeTableCell(table, "Actual Start Time");
            writeTableCell(table, startTimeStr);

            Date actualStopTime = optimizingJob.getActualStopTime();
            String stopTimeStr;
            if (actualStopTime == null) {
                stopTimeStr = "(Not Available)";
            } else {
                stopTimeStr = dateFormat.format(actualStopTime);
            }
            writeTableCell(table, "Actual Stop Time");
            writeTableCell(table, stopTimeStr);

            Job[] iterations = optimizingJob.getAssociatedJobs();
            if ((iterations != null) && (iterations.length > 0)) {
                writeTableCell(table, "Job Iterations Completed");
                writeTableCell(table, String.valueOf(iterations.length));

                int optimalThreadCount = optimizingJob.getOptimalThreadCount();
                String threadStr;
                if (optimalThreadCount > 0) {
                    threadStr = String.valueOf(optimalThreadCount);
                } else {
                    threadStr = "(Not Available)";
                }
                writeTableCell(table, "Optimal Thread Count");
                writeTableCell(table, threadStr);

                double optimalValue = optimizingJob.getOptimalValue();
                String valueStr;
                if (optimalThreadCount > 0) {
                    valueStr = decimalFormat.format(optimalValue);
                } else {
                    valueStr = "(Not Available)";
                }
                writeTableCell(table, "Optimal Value");
                writeTableCell(table, valueStr);

                String optimalID = optimizingJob.getOptimalJobID();
                writeTableCell(table, "Optimal Job Iteration");
                if ((optimalID == null) || (optimalID.length() == 0)) {
                    writeTableCell(table, "(Not Available)");
                } else if (includeOptimizingIterations) {
                    anchor = new Anchor(optimalID,
                            FontFactory.getFont(FontFactory.HELVETICA, 12, Font.UNDERLINE, Color.BLUE));
                    anchor.setReference('#' + optimalID);
                    table.addCell(new PdfPCell(anchor));
                } else {
                    writeTableCell(table, optimalID);
                }
            }

            Job reRunIteration = optimizingJob.getReRunIteration();
            if (reRunIteration != null) {
                writeTableCell(table, "Re-Run Iteration");
                if (includeOptimizingIterations) {
                    anchor = new Anchor(reRunIteration.getJobID(),
                            FontFactory.getFont(FontFactory.HELVETICA, 12, Font.UNDERLINE, Color.BLUE));
                    anchor.setReference('#' + reRunIteration.getJobID());
                    table.addCell(new PdfPCell(anchor));
                } else {
                    writeTableCell(table, reRunIteration.getJobID());
                }

                String valueStr;
                try {
                    double iterationValue = optimizationAlgorithm.getIterationOptimizationValue(reRunIteration);
                    valueStr = decimalFormat.format(iterationValue);
                } catch (Exception e) {
                    valueStr = "N/A";
                }

                writeTableCell(table, "Re-Run Iteration Value");
                writeTableCell(table, valueStr);
            }

            document.add(table);

            if (includeOptimizingIterations && (iterations != null) && (iterations.length > 0)) {
                document.add(new Paragraph(" "));
                p = new Paragraph("Job Iterations",
                        FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
                document.add(p);

                table = new PdfPTable(2);
                table.setWidthPercentage(100);
                table.setWidths(new int[] { 50, 50 });

                for (int i = 0; i < iterations.length; i++) {
                    String valueStr;
                    try {
                        double iterationValue = optimizationAlgorithm.getIterationOptimizationValue(iterations[i]);
                        valueStr = decimalFormat.format(iterationValue);
                    } catch (Exception e) {
                        valueStr = "N/A";
                    }

                    anchor = new Anchor(iterations[i].getJobID(),
                            FontFactory.getFont(FontFactory.HELVETICA, 12, Font.UNDERLINE, Color.BLUE));
                    anchor.setReference('#' + iterations[i].getJobID());
                    table.addCell(new PdfPCell(anchor));
                    writeTableCell(table, valueStr);
                }

                document.add(table);
            }

            if (includeGraphs && (iterations != null) && (iterations.length > 0)) {
                String[] statNames = iterations[0].getStatTrackerNames();
                for (int j = 0; j < statNames.length; j++) {
                    StatTracker[] trackers = iterations[0].getStatTrackers(statNames[j]);
                    if ((trackers != null) && (trackers.length > 0)) {
                        StatTracker tracker = trackers[0].newInstance();
                        tracker.aggregate(trackers);

                        try {
                            document.newPage();
                            ParameterList params = tracker.getGraphParameterStubs(iterations);
                            BufferedImage graphImage = tracker.createGraph(iterations,
                                    Constants.DEFAULT_GRAPH_WIDTH, Constants.DEFAULT_GRAPH_HEIGHT, params);
                            Image image = Image.getInstance(imageToByteArray(graphImage));
                            image.scaleToFit(inchesToPoints(5.5), inchesToPoints(4.5));
                            document.add(image);
                        } catch (Exception e) {
                        }
                    }
                }
            }

            if (includeOptimizingIterations && (iterations != null) && (iterations.length > 0)) {
                for (int i = 0; i < iterations.length; i++) {
                    document.newPage();
                    writeJob(document, iterations[i]);
                }
            }
            if (includeOptimizingIterations && (reRunIteration != null)) {
                document.newPage();
                writeJob(document, reRunIteration);
            }
        }
    }

    /**
     * Writes the specified text to the provided table as a header cell.
     *
     * @param  table  The table to which the header cell should be written.
     * @param  text   The text to write to the header cell.
     */
    private void writeTableHeaderCell(PdfPTable table, String text) {
        Phrase phrase = new Phrase(text, FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, Color.BLACK));
        table.addCell(new PdfPCell(phrase));
    }

    /**
     * Writes the specified text to the provided table as a normal cell.
     *
     * @param  table  The table to which the cell should be written.
     * @param  text   The text to write to the cell.
     */
    private void writeTableCell(PdfPTable table, String text) {
        table.addCell(new PdfPCell(new Phrase(text)));
    }

    /**
     * Converts the specified number of inches into points (there are 72 points
     * per inch).  The number of inches provided does not need to be an integer.
     *
     * @param  numInches  The number of inches to be converted to points.
     *
     * @return  The number of points corresponding to the provided number of
     *          inches.
     */
    public static int inchesToPoints(double numInches) {
        return (int) Math.round(numInches * 72);
    }

    /**
     * Converts the provided image to a byte array containing data for the PNG
     * representation of the image.
     *
     * @param  image  The image to be converted to a byte array.
     *
     * @return  The byte array containing the image data.
     *
     * @throws  IOException  If a problem occurs while creating the image array.
     */
    private byte[] imageToByteArray(BufferedImage image) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(8192);
        ImageEncoder encoder = ImageCodec.createImageEncoder("png", outputStream, null);
        encoder.encode(image);
        return outputStream.toByteArray();
    }

    /**
     * Performs the appropriate action necessary when starting a new page.  In
     * this case, we will write the SLAMD header to the top of the page.
     *
     * @param  writer    The writer used to write the PDF document.
     * @param  document  The PDF document being written.
     */
    public void onStartPage(PdfWriter writer, Document document) {
        try {
            PdfPTable table = new PdfPTable(3);
            table.setWidthPercentage(100);

            PdfPCell blueCell = new PdfPCell(new Phrase(" \n "));
            blueCell.setHorizontalAlignment(PdfPCell.ALIGN_LEFT);
            blueCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
            blueCell.setBackgroundColor(new Color(0x59, 0x4F, 0xBF));
            blueCell.setBorderWidth(inchesToPoints(1.0 / 16));
            blueCell.setBorderColor(new Color(0xFF, 0xFF, 0xFF));
            blueCell.setPadding(inchesToPoints(1.0 / 16));
            table.addCell(blueCell);

            Phrase titlePhrase = new Phrase("SLAMD Generated Report",
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, new Color(0x59, 0x4F, 0xBF)));
            PdfPCell yellowCell = new PdfPCell(titlePhrase);
            yellowCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
            yellowCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
            yellowCell.setBackgroundColor(new Color(0xFB, 0xE2, 0x49));
            yellowCell.setBorderWidth(inchesToPoints(1.0 / 16));
            yellowCell.setBorderColor(new Color(0xFF, 0xFF, 0xFF));
            yellowCell.setPadding(inchesToPoints(1.0 / 16));
            table.addCell(yellowCell);

            Phrase versionPhrase = new Phrase("Version " + DynamicConstants.SLAMD_VERSION,
                    FontFactory.getFont(FontFactory.HELVETICA, 12, Font.BOLD, new Color(0xFF, 0xFF, 0xFF)));
            PdfPCell redCell = new PdfPCell(versionPhrase);
            redCell.setHorizontalAlignment(Cell.ALIGN_RIGHT);
            redCell.setVerticalAlignment(Cell.ALIGN_MIDDLE);
            redCell.setBackgroundColor(new Color(0xD1, 0x21, 0x24));
            redCell.setBorderWidth(inchesToPoints(1.0 / 16));
            redCell.setBorderColor(new Color(0xFF, 0xFF, 0xFF));
            redCell.setPadding(inchesToPoints(1.0 / 16));
            table.addCell(redCell);

            document.add(table);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Performs the appropriate action necessary when ending a page.  In this
     * case, no action is required.
     *
     * @param  writer    The writer used to write the PDF document.
     * @param  document  The PDF document being written.
     */
    public void onEndPage(PdfWriter writer, Document document) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when opening a document.  In this
     * case, no action is required.
     *
     * @param  writer    The writer used to write the PDF document.
     * @param  document  The PDF document being written.
     */
    public void onOpenDocument(PdfWriter writer, Document document) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when opening a document.  In this
     * case, no action is required.
     *
     * @param  writer    The writer used to write the PDF document.
     * @param  document  The PDF document being written.
     */
    public void onCloseDocument(PdfWriter writer, Document document) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when starting a new paragraph.
     * In this case, no action is required.
     *
     * @param  writer        The writer used to write the PDF document.
     * @param  document      The PDF document being written.
     * @param  paragraphPos  The position of the beginning of the paragraph.
     */
    public void onParagraph(PdfWriter writer, Document document, float paragraphPos) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when ending a paragraph.  In this
     * case, no action is required.
     *
     * @param  writer           The writer used to write the PDF document.
     * @param  document         The PDF document being written.
     * @param  paragraphEndPos  The position of the end of the paragraph.
     */
    public void onParagraphEnd(PdfWriter writer, Document document, float paragraphEndPos) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when starting a new chapter.  In
     * this case, no action is required.
     *
     * @param  writer      The writer used to write the PDF document.
     * @param  document    The PDF document being written.
     * @param  chapterPos  The position at which the beginning of the chapter will
     *                     be written.
     * @param  title       The title to use for the chapter.
     */
    public void onChapter(PdfWriter writer, Document document, float chapterPos, Paragraph title) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when ending a chapter.  In this
     * case, no action is required.
     *
     * @param  writer         The writer used to write the PDF document.
     * @param  document       The PDF document being written.
     * @param  chapterEndPos  The position at which the end of the chapter will be
     *                        written.
     */
    public void onChapterEnd(PdfWriter writer, Document document, float chapterEndPos) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when beginning a new section.  In
     * this case, no action is required.
     *
     * @param  writer        The writer used to write the PDF document.
     * @param  document      The PDF document being written.
     * @param  sectionPos    The position at which the beginning of the section
     *                       will be written.
     * @param  depth         The depth for the section.
     * @param  sectionTitle  The title to use for the section.
     */
    public void onSection(PdfWriter writer, Document document, float sectionPos, int depth,
            Paragraph sectionTitle) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when ending a section.  In this
     * case, no action is required.
     *
     * @param  writer         The writer used to write the PDF document.
     * @param  document       The PDF document being written.
     * @param  sectionEndPos  The position at which the end of the section will be
     *                        written.
     */
    public void onSectionEnd(PdfWriter writer, Document document, float sectionEndPos) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }

    /**
     * Performs the appropriate action necessary when writing a generic tag.  In
     * this case, no action is required.
     *
     * @param  writer     The writer used to write the PDF document.
     * @param  document   The PDF document being written.
     * @param  rectangle  The rectangle containing the chunk with the generic tag.
     * @param  text       The text of the tag.
     */
    public void onGenericTag(PdfWriter writer, Document document, Rectangle rectangle, String text) {
        // No action necessary, but this method is required by the PdfPageEvent
        // interface.
    }
}