/*
** Module   : ReportGenerator.java
** Abstract : routines for generating test result reports
**
** Copyright (c) 2009-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090202 Routines for generating test result reports.
** 002 HC  20140922 Test-step report description field is now HTML-escaped.
** 003 CA  20210827 Refactored to allow other test types, beside terminal (which can be executed from the same 
**                  test suite).  Added SOAP and REST web service tests.
** 004 CA  20210902 A test will wrap the content of 'pre' tags.
** 005 CA  20210906 The content for description and failure is wrapped only for web tests. 
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness;

import java.io.*;
import java.util.*;

import com.goldencode.io.*;
import com.goldencode.html.*;
import com.goldencode.util.*;
import com.goldencode.harness.test.*;

/**
 * Routines for generating test result reports.
 */
public class ReportGenerator
extends TableHelper
{
   /** Array of status code types. */
   private static final Status[] keys = Status.values();
   
   /** Main report name for test plans and test sets. */
   private static final String INDEX_NAME = "index.html";
   
   /** Format specification for a test set specific sub-directory. */
   private static final String TEST_SET_DIR_SPEC = "%stest_set_%d_%s%s";
   
   /** Format specification for a test-specific filename. */
   private static final String TEST_FILE_SPEC = "%stest_%d_%s.html";
   
   /** Test plan for which to generate a report. */
   private final TestPlan testPlan;
   
   /** Object in which to store the details of failures. */
   private final FailureNotification rc;
   
   /**
    * Create an instance.
    *
    * @param    testPlan
    *           The test plan for which to generate a report. 
    * @param    rc
    *           Object in which to store the details of failures.
    */
   public ReportGenerator(TestPlan testPlan, FailureNotification rc)
   {
      this.testPlan = testPlan;
      this.rc       = rc;
   }
   
   /**
    * Generate all results reports for the entire test plan, all included
    * test sets and tests. This must only be called AFTER all test execution
    * and post processing has been completed.
    * <p>
    * The following methods are used (in this order):
    * <p>
    * <ol>
    *    <li> {@link #generateTestPlan}
    *    <li> {@link #generateTestSetReports}
    * </ol>
    */
   public void generate()
   {
      // this ordering is important since the test set processing calculates
      // URLs that are needed to link from the main report to the test set
      // reports
      generateTestSetReports();
      
      if (rc.isFailed())
      {
         return;
      }
      
      generateTestPlan();
   }
   
   /**
    * Open a file and write a summary report for the test plan. This report is
    * the "top-level" report for the entire results set. The report will have
    * the following sections:
    * <p>
    * <ol>
    *    <li> Overall Results - this describes the results at a test plan level
    *         and is generated using {@link #generateReportSummary}.
    *    <li> Status Code Summary - this displays frequency counters for each
    *         status code for all test sets, all the tests within each test set
    *         and for all tests accumulated over all test sets. See
    *         {@link #generateStatsSummary}.
    *    <li> Test Set Results - this summarizes the results of each test set
    *         and is generated using {@link #generateReportTable}.
    * </ol>
    * <p>
    * The filename for the report will be generated and will exist in the
    * test plan's configured output directory.
    */
   public void generateTestPlan()
   {
      // create test plan report name
      StringBuilder sb = new StringBuilder();
      sb.append(testPlan.getOutputDirectory()).append(INDEX_NAME);
      generateTestPlan(sb.toString());
   }
   
   /**
    * Open a file and write a summary report for the test plan. This report is
    * the "top-level" report for the entire results set. The report will have
    * the following sections:
    * <p>
    * <ol>
    *    <li> Overall Results - this describes the results at a test plan level
    *         and is generated using {@link #generateReportSummary}.
    *    <li> Status Code Summary - this displays frequency counters for each
    *         status code for all test sets, all the tests within each test set
    *         and for all tests accumulated over all test sets. See
    *         {@link #generateStatsSummary}.
    *    <li> Test Set Results - this summarizes the results of each test set
    *         and is generated using {@link #generateReportTable}.
    * </ol>
    *
    * @param    fname
    *           Output filename.
    */
   public void generateTestPlan(String fname)
   {
      IndentingWriter out = createStream(fname);
      
      try
      {
         // write document header
         writeHeader(out, "Test Plan Summary");
         writeSeparator(out);         
         
         generateReportSummary(out, testPlan, false, "Test Plan", "Test Set(s)");
         writeSeparator(out);
         
         // status codes by tests in test sets
         StatusCounter[] list = makeCounterArray(testPlan.getSets());
         generateStatsSummary(out, list, "Test Set");
         writeSeparator(out);
         
         // output test set results
         generateReportTable(out,
                             testPlan.getSets(),
                             "Test Set Results",
                             false,
                             false);
         writeSeparator(out);
         
         // write document footer
         writeSimpleFooter(out);
      }
      
      catch (Exception exc)
      {
         rc.setResult(ExitCode.UNKNOWN_FAILURE);
         rc.setDetails(LogHelper.dumpThrowable(exc));
      }
      
      finally
      {
         close(out);
      }
   }
   
   /**
    * Iterate through all test sets in the test plan and for each one, create a
    * test set report in the given output directory as well as reports for each
    * test in the test set. Reports are created using
    * {@link #generateTestReports} and {@link #generateTestSetOutput}.
    */
   public void generateTestSetReports()
   {
      List<TestSet>     sets = testPlan.getSets();
      Iterator<TestSet> iter = sets.iterator();
      
      int num = 0;
      
      while (iter.hasNext())
      {
         num++;
         
         TestSet set = iter.next();
         
         String sname = FileSystem.makeSafeFilename(set.getName());
         
         // calculate and create a test set specific sub-directory name
         String dir = String.format(TEST_SET_DIR_SPEC,
                                    testPlan.getOutputDirectory(),
                                    num,
                                    sname,
                                    File.separator);
         
         if (!FileSystem.createPath(dir))
         {
            rc.setResult(ExitCode.INVALID_PATHNAME);
            rc.setDetails(dir);
            return;
         }
         
         // generate a report for each test in that directory
         generateTestReports(set, dir);
         
         // generate the main test set summary/index report
         String fname = String.format("%s%s", dir, INDEX_NAME);
         generateTestSetOutput(set, fname);
         
         // create the URL to access that filename and store it
         String link = String.format(TEST_SET_DIR_SPEC,
                                     "",
                                     num,
                                     sname,
                                     File.separator);
         link = String.format("%s%s", link, INDEX_NAME);
         set.setURL(HtmlHelper.href(link, set.getName(), false));
      }
   }
   
   /**
    * Generate the detailed results for a single test set. The report will have
    * the following sections:
    * <p>
    * <ol>
    *    <li> Overall Results - this describes the results at a test set level
    *         and is generated using {@link #generateReportSummary}.
    *    <li> Status Code Summary - this displays frequency counters for each
    *         status code for all tests. See {@link #generateStatsSummary}.
    *    <li> Test Set Results - this summarizes the results of each test
    *         and is generated using {@link #generateReportTable}.
    * </ol>
    *
    * @param    set
    *           The test set to report on.
    * @param    fname
    *           Output filename in which to write the HTML.
    */
   public void generateTestSetOutput(TestSet set, String fname)
   {
      IndentingWriter out = createStream(fname);
      
      try
      {
         String tname = set.getName();
         
         if (tname != null && tname.length() > 0)
         {
            tname = String.format("Test Set Result - %s", tname);
         }
         else
         {
            tname = "Test Set Result";
         }
         
         // write document header
         writeHeader(out, tname);
         writeSeparator(out);
         
         // overall test summary
         generateReportSummary(out, set, false, "Test Set", "Test(s)");
         writeSeparator(out);
         
         // session settings summary
         generateTargetSummary(out, set.getCfgs());
         writeSeparator(out);
         
         // status codes for the steps of the tests
         StatusCounter[] list = makeCounterArray(set.getTests());
         generateStatsSummary(out, list, "Test");
         writeSeparator(out);
         
         // output step-level results
         generateReportTable(out, set.getTests(), "Test Results", true, false);
         writeSeparator(out);
         
         // write document footer
         writeSimpleFooter(out);
      }
      
      catch (Exception exc)
      {
         rc.setResult(ExitCode.UNKNOWN_FAILURE);
         rc.setDetails(LogHelper.dumpThrowable(exc));
      }
      
      finally
      {
         close(out);
      }
   }
   
   /**
    * Write a report section that summarizes the settings used to access
    * a given target system.
    *
    * @param    out
    *           Output destination.
    * @param    cfgs
    *           Settings to write.
    */
   public void generateTargetSummary(IndentingWriter out, Map<String, SessionSettings> cfgs)
   {
      if (cfgs == null || cfgs.isEmpty())
      {
         return;
      }
      
      for (String cfgType : cfgs.keySet())
      {
         SessionSettings cfg = cfgs.get(cfgType);
         
         // track if this row's background needs to be colorized or not
         boolean color = false;
         
         writeHeading(out, 3, "Session Configuration - " + cfgType);
         
         // start table
         writeTableOpen(out);         
         
         String[][] configs = cfg.getConfigs();
         for (int i = 0; i < configs.length; i++)
         {
            writeRowStart(out, color, null);
            writeField(out,
                       configs[i][0],
                       VALIGN_MIDDLE,
                       HALIGN_LEFT,
                       BOLD);
            writeField(out,
                       configs[i][1],
                       VALIGN_MIDDLE,
                       HALIGN_LEFT,
                       NORMAL);
            writeRowEnd(out);
               
            // toggle this value for the next row
            color = !color;
         }
            
         // end the table
         writeTableClose(out);
      }
   }
   
   /**
    * Iterate through all tests in the test set and so each one, create a
    * test-specific report in the given output directory. Each report is
    * created using {@link #generateTestOutput}.
    *
    * @param    set
    *           The tests to report on.
    * @param    dir
    *           Output directory.
    */
   public void generateTestReports(TestSet set, String dir)
   {
      List<Test>     tests = set.getTests();
      Iterator<Test> iter  = tests.iterator();
      
      int num = 0;
      
      while (iter.hasNext())
      {
         num++;
         
         Test test = iter.next();
         
         // calculate a test-specific filename
         String tname = FileSystem.makeSafeFilename(test.getName());
         String fname = String.format(TEST_FILE_SPEC, dir, num, tname);
         
         // write the test-specific report
         generateTestOutput(test, fname);
         
         // create the URL to access that filename and store it
         String link = String.format(TEST_FILE_SPEC, "", num, tname);
         test.setURL(HtmlHelper.href(link, test.getName(), false));
      }
   }
   
   /**
    * Generate the detailed results for a single test. The report will have
    * the following sections:
    * <p>
    * <ol>
    *    <li> Overall Results - this describes the results at a test level
    *         and is generated using {@link #generateReportSummary}.
    *    <li> Status Code Summary - this displays frequency counters for each
    *         status code for all steps. See {@link #generateStatsSummary}.
    *    <li> Test Set Results - this summarizes the results of each step
    *         and is generated using {@link #generateReportTable}.
    * </ol>
    *
    * @param    test
    *           The test to report on.
    * @param    fname
    *           Output filename in which to write the HTML.
    */
   public void generateTestOutput(Test test, String fname)
   {
      IndentingWriter out = createStream(fname);
      
      try
      {
         String tname = test.getName();
         
         if (tname != null && tname.length() > 0)
         {
            tname = String.format("Test Result - %s", tname);
         }
         else
         {
            tname = "Test Result";
         }
         
         // write document header
         writeHeader(out, tname);
         writeSeparator(out);
         
         // overall test summary
         generateReportSummary(out, test, true, "Test", "Step(s)");
         writeSeparator(out);
         
         // status codes for the steps
         StatusCounter[] list = new StatusCounter[1];
         list[0] = test.getStatusCounter();
         
         if (list[0] == null)
         {
            // status counter calculation may not have occurred on aborted
            // tests
            test.calculateStatusCounter(test.getSteps());
            list[0] = test.getStatusCounter();
         }
         
         list[0].setDescription("Steps");
         generateStatsSummary(out, list, "");
         writeSeparator(out);
         
         // output step-level results
         generateReportTable(out, test.getSteps(), "Step Results", false, true);
         writeSeparator(out);
         
         // write document footer
         writeSimpleFooter(out);
      }
      
      catch (Exception exc)
      {
         rc.setResult(ExitCode.UNKNOWN_FAILURE);
         rc.setDetails(LogHelper.dumpThrowable(exc));
      }
      
      finally
      {
         close(out);
      }
   }
   
   /**
    * Generate a summary table of a given report element.
    *
    * @param    out
    *           Output destination.
    * @param    report
    *           Report element providing the summary data.
    * @param    failMode
    *           <code>true</code> to emit failure mode info.
    * @param    mainDescr
    *           Descriptive name for the report.
    * @param    elementDescr
    *           Descriptive name for the report sub-elements.
    */
   public void generateReportSummary(IndentingWriter out,
                                     Report          report,
                                     boolean         failMode,
                                     String          mainDescr,
                                     String          elementDescr)
   {
      // track if this row's background needs to be colorized or not
      boolean color = false;
      
      writeHeading(out, 3, String.format("%s Results", mainDescr));
      
      // start table
      writeTableOpen(out);         
      
      // row 0 - thread name
      writeRowStart(out, color, null);
      writeField(out,
                 "Name",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 makeNonEmpty(report.getName()),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL,
                 true);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 1 - start date/time
      writeRowStart(out, color, null);
      writeField(out,
                 "Start Date/Time",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 String.format("%1$Tm/%1$Td/%1$TY %1$TT %1$Tz",
                               report.getStartTime()),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 2 - end date/time
      writeRowStart(out, color, null);
      writeField(out,
                 "End Date/Time",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 String.format("%1$Tm/%1$Td/%1$TY %1$TT %1$Tz",
                               report.getEndTime()),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 3 - name
      writeRowStart(out, color, null);
      writeField(out,
                 "Execution Thread",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 makeNonEmpty(report.getThreadName()),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL,
                 true);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 4 - description
      writeRowStart(out, color, null);
      writeField(out,
                 "Description",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 makeNonEmpty(report.getDescription()),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL,
                 true);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 5 - number of sub-elements
      writeRowStart(out, color, null);
      writeField(out,
                 String.format("Number of %s", elementDescr),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 Integer.toString(report.getNumberOfElements()),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 6 - status
      writeRowStart(out, color, null);
      writeField(out,
                 "Status",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 report.getStatus().toString(),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 7 - dependency
      writeRowStart(out, color, null);
      writeField(out,
                 "Dependency",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 report.getDependency().toString(),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      // row 7b - optional failure mode
      if (failMode)
      {
         writeRowStart(out, color, null);
         writeField(out,
                    "Failure Mode",
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    BOLD);
         writeField(out,
                    report.getFailureMode().toString(),
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    NORMAL);
         writeRowEnd(out);
            
         // toggle this value for the next row
         color = !color;
      }
      
      // row 8 - elapsed time
      writeRowStart(out, color, null);
      writeField(out,
                 "Cumulative Time (Seconds)",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 String.format("%.3f", (report.getElapsed() / 1000.0)),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL);
      writeRowEnd(out);
         
      // toggle this value for the next row
      color = !color;
      
      String reason = report.getReason();
      
      // row 9 - reasons
      writeRowStart(out, color, null);
      writeField(out,
                 "Failure Reason(s)",
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 BOLD);
      writeField(out,
                 makeNonEmpty((reason == null) ? "n/a" : reason),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 PREFORMAT,
                 true);
      writeRowEnd(out);
         
      // end the table
      writeTableClose(out);
   }
   
   /**
    * Output a table of results with a row for each report element in the
    * list.
    *
    * @param    out
    *           Output destination.
    * @param    list
    *           Report elements that comprise the tables contents.
    * @param    title
    *           The table's title.
    * @param    test
    *           <code>true</code> if this is a test level report. Such reports
    *           have failure mode details included.    
    * @param    step
    *           <code>true</code> if this is a step level report. Step level
    *           reports have a description field that need the
    *           <code>PREFORMAT</code> output type. In addition, no thread
    *           or dependency data is output for step level reports.
    */
   public void generateReportTable(IndentingWriter            out,
                                   List<? extends Reportable> list,
                                   String                     title,
                                   boolean                    test,
                                   boolean                    step)
   {
      // write a table name for the category
      writeHeading(out, 3, title);
      
      // track if this row's background needs to be colorized or not
      boolean color = false;
      
      // start table
      writeTableOpen(out);         
      
      // write table header row
      writeRowStart(out, true, OLIVE);
      writeField(out, "Number", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
      writeField(out, "Name", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
      writeField(out, "Description", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
      writeField(out, "Status", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
      if (!step)
      {
         writeField(out, "Dependency", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
         if (test)
            writeField(out, "Failure Mode", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
         writeField(out, "Thread", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
      }
      writeField(out,
                 "Cumulative Time (Seconds)",
                 VALIGN_MIDDLE,
                 HALIGN_CENTER,
                 BOLD);
      writeField(out, "Failure Reason(s)", VALIGN_MIDDLE, HALIGN_CENTER, BOLD);
      writeRowEnd(out);
      
      Iterator<? extends Reportable> iter = list.iterator();
      
      int num = 0;
      
      // write a table row for each report element
      while (iter.hasNext())
      {
         Reportable next = iter.next();
         boolean wrap = next.isWrappedContent();
         num++;
         
         String url    = next.getURL();
         String name   = null;
         String descr  = next.getDescription();
         String reason = next.getReason();
         
         if (url != null && url.length() > 0)
         {
            name = url;
         }
         else
         {
            name = makeNonEmpty(next.getName());
         }
         
         writeRowStart(out, color, null);
         
         // emit the fields for the new row
         writeField(out,
                    Integer.toString(num),
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    NORMAL);
         writeField(out,
                    name,
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    NORMAL,
                    true);
         
         descr = makeNonEmpty(descr);
         if (step)
         {
            // the step description may contain program output and html-unsafe chars
            descr = HtmlHelper.filter(descr);
         }
         
         writeField(out,
                    honorNewLine(descr),
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    step ? (wrap ? PREFORMAT_WRAP : PREFORMAT) : NORMAL,
                    true);
         writeField(out,
                    next.getStatus().toString(),
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    NORMAL);
         if (!step)
         {
            writeField(out,
                       next.getDependency().toString(),
                       VALIGN_MIDDLE,
                       HALIGN_LEFT,
                       NORMAL);
            if (test)
            {
               writeField(out,
                          next.getFailureMode().toString(),
                          VALIGN_MIDDLE,
                          HALIGN_LEFT,
                          NORMAL);
            }
            writeField(out,
                       makeNonEmpty(next.getThreadName()),
                       VALIGN_MIDDLE,
                       HALIGN_LEFT,
                       NORMAL,
                       true);
         }
         writeField(out,
                    String.format("%.3f", (next.getElapsed() / 1000.0)),
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    NORMAL);
         writeField(out,
                    honorNewLine(makeNonEmpty(reason)),
                    VALIGN_MIDDLE,
                    HALIGN_LEFT,
                    wrap ? PREFORMAT_WRAP : PREFORMAT,
                    true);
         
         writeRowEnd(out);
         
         // toggle this value for the next row
         color = !color;
      }
      
      // end the table
      writeTableClose(out);
   }
   
   /**
    * Obtain the counter for each of the given reports and convert this into
    * an array.
    *
    * @param    rpts
    *           Source for the counters.
    * 
    * @return   Array of counters whose elements correspond with the given
    *           list of reports.
    */
   public StatusCounter[] makeCounterArray(List<? extends Reportable> rpts)
   {
      Iterator<? extends Reportable> iter = rpts.iterator();
      
      int len = rpts.size();
      
      StatusCounter[] counters = new StatusCounter[len + 1];
      
      // track the totals too as the last entry
      counters[len] = new StatusCounter();
      counters[len].setDescription("Total");
      
      int num = 0;
      int idx = 0;
      
      while (iter.hasNext())
      {
         num++;
         
         Reportable report = iter.next();
         
         String name = String.format("%d - %s", num, report.getName()); 
         
         counters[idx] = report.getStatusCounter();
         counters[idx].setDescription(name);
         counters[len].accumulate(counters[idx]);
         
         idx++;
      }
      
      return counters;
   }
   
   /**
    * Start a summary table of the status codes and write the first row (the
    * headings).
    *
    * @param    out
    *           Output destination.
    * @param    descr
    *           Description for the resource being reported on.
    */
   public void writeStatsHeader(IndentingWriter out, String descr)
   {
      writeHeading(out, 3, "Status Code Summary");
      
      // start table
      writeTableOpen(out);         
      
      // create heading
      writeRowStart(out, true, OLIVE);
      writeField(out,
                 makeNonEmpty(descr),
                 VALIGN_MIDDLE,
                 HALIGN_LEFT,
                 NORMAL,
                 true);
      for (int x = 0; x < keys.length; x++)
      {
         writeField(out, keys[x].toString(), VALIGN_MIDDLE, HALIGN_LEFT, BOLD);
      }
      writeRowEnd(out);
   }
   
   /**
    * Generate a summary table of the status codes based on the list of
    * given statistics.
    *
    * @param    out
    *           Output destination.
    * @param    counters
    *           Statistics for which to output row content (1 row per counter).
    * @param    descr
    *           Description for the resource being reported on.
    */
   public void generateStatsSummary(IndentingWriter out,
                                    StatusCounter[] counters,
                                    String          descr)
   {
      writeStatsHeader(out, descr);
      
      // track if this row's background needs to be colorized or not
      boolean color = false;
      
      for (int i = 0; i < counters.length; i++)
      {
         color = writeStatsRow(out, counters[i], color);
      }
      
      // end the table
      writeTableClose(out);
   }
   
   /**
    * Helper to output status counter statistics as a table row.
    *
    * @param    out
    *           Output destination.
    * @param    counter
    *           Statistics to output.
    * @param    color
    *           If <code>true</code>, the background of this row will not
    *           use the table or document background color.
    * 
    * @return   The negated value of the <code>color</code> parameter.
    */
   public boolean writeStatsRow(IndentingWriter out,
                                StatusCounter   counter,
                                boolean         color)
   {
      Status[] keys = Status.values();
      
      writeRowStart(out, color, null);
      
      // 1st column
      String descr = counter.getDescription();
      writeField(out, descr, VALIGN_MIDDLE, HALIGN_LEFT, BOLD);
      
      // stats columns
      for (int x = 0; x < keys.length; x++)
      {
         String val = Integer.toString(counter.getCounter(keys[x]));
         writeField(out, val, VALIGN_MIDDLE, HALIGN_RIGHT, NORMAL);
      }
      
      writeRowEnd(out);
      
      // toggle this value for the next row
      return !color;
   }
}
