/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.phoenix.report;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.jcoderz.commons.types.Date;
import org.jcoderz.commons.util.ArraysUtil;
import org.jcoderz.commons.util.Assert;
import org.jcoderz.commons.util.Constants;
import org.jcoderz.commons.util.EmptyIterator;
import org.jcoderz.commons.util.FileUtils;
import org.jcoderz.commons.util.IoUtil;
import org.jcoderz.commons.util.LoggingUtils;
import org.jcoderz.commons.util.ObjectUtil;
import org.jcoderz.commons.util.StringUtil;
import org.jcoderz.commons.util.XmlUtil;
import org.jcoderz.phoenix.report.jaxb.Item;
import org.jcoderz.phoenix.report.jaxb.Report;
/**
* TODO: Link to current build
* TODO: Link to CC home
* TODO: Add @media printer???
* TODO: Refactor, split class.
*
* @author Andreas Mandel
*/
public final class Java2Html
{
/** property name for the wiki url prefix. */
public static final String WIKI_BASE_PROPERTY = "report.wiki-prefix";
/** Pattern helper to generate css style names of the listings. */
private static final String[] PATTERN = {"odd", "even"};
/** Size of the pattern. */
private static final int PATTERN_SIZE = PATTERN.length;
/** Name of this class. */
private static final String CLASSNAME = Java2Html.class.getName();
/** The logger used for technical logging inside this class. */
private static final Logger logger = Logger.getLogger(CLASSNAME);
/** String used as line separator in the output html. */
private static final String NEWLINE = "\n";
/** Name of the index page with content sorted by package name. */
private static final String SORT_BY_PACKAGE_INDEX = "index.html";
/** Name of the index page with content sorted by quality. */
private static final String SORT_BY_QUALITY_INDEX = "index_q.html";
/** Name of the index page with content sorted by coverage. */
private static final String SORT_BY_COVERAGE_INDEX = "index_c.html";
/** Marker for ccs styles used as the last row in a table. */
private static final String LAST_MARKER = "_last";
private static final String DEFAULT_STYLESHEET = "reportstyle.css";
private static final int NUMBER_OF_AGE_SEGMENTS = 5;
/** Name to be used for unnamed package. */
private static final String UNNAMED_PACKAGE_NAME = "unnamed-package";
/**
* Only findings that span at maximum this number of lines are
* highlighted in the code.
*/
private static final int MAX_LINES_WITH_INLINE_MARK = 3;
private static final int ABSOLUTE_END_OF_LINE = 9999;
/** Default tab with to use. */
private static final int DEFAULT_TAB_WIDTH = 8;
/** Collects a List of all <code>FileSummary</code>s of the report. */
private final List<FileSummary> mAllFiles
= new ArrayList<FileSummary>();
/** Map of package name + FileSummary for this package. */
private final Map<String, FileSummary> mPackageSummary
= new HashMap<String, FileSummary>();
private final Map<String, List<FileSummary>> mAllPackages
= new HashMap<String, List<FileSummary>>();
/**
* Collects findings in the current file.
* Maps from the line number (Integer) to a List of Item objects.
*/
private final Map<Integer, List<Item>> mFindingsInFile
= new HashMap<Integer, List<Item>>();
private final Set<Item> mFindingsInCurrentLine
= new HashSet<Item>();
private final List<Item> mCurrentFindings = new ArrayList<Item>();
private final List<Item> mHandledFindings
= new ArrayList<Item>();
/** List of findings with no (available) file assignment */
private final List<org.jcoderz.phoenix.report.jaxb.File> mGlobalFindings
= new ArrayList<org.jcoderz.phoenix.report.jaxb.File>();
/** file summary for all files */
private FileSummary mGlobalSummary;
private String mProjectName = "";
private String mWebVcBase = null;
private String mWebVcSuffix = "";
private String mProjectHome;
private String mTimestamp = null;
private String mStyle = DEFAULT_STYLESHEET; // the CSS stuff to use
private String mClassname;
private String mPackage;
private String mPackageBase;
private final StringBuilder mStringBuilder
= new StringBuilder();
/** String buffer to be used by the getIcons method. */
private final StringBuilder mGetIconsStringBuffer
= new StringBuilder();
private java.io.File mInputData;
private java.io.File mOutDir;
private boolean mCoverageData = true;
private Level mLogLevel = Level.INFO;
/**
* Holds a list of items that are currently active for the
* current file and line.
*/
private final List<Item> mActiveItems
= new LinkedList<Item>();
private Charset mCharSet = Charset.defaultCharset();
private int mTabWidth = DEFAULT_TAB_WIDTH;
/** The full Report. */
private Report mReport;
private final Map<Item, org.jcoderz.phoenix.report.jaxb.File> mItemToFileMap
= new HashMap<Item, org.jcoderz.phoenix.report.jaxb.File>();
/**
* Constructor.
*
* @throws IOException In case the current working directory cannot be
* determined.
*/
public Java2Html ()
throws IOException
{
mProjectHome = new java.io.File(".").getCanonicalPath();
}
/**
* Main entry point.
*
* @param args The command line arguments.
* @throws IOException an io exception occurs.
* @throws JAXBException if the xml can not be parsed.
*/
public static void main (String[] args)
throws IOException, JAXBException
{
final Java2Html engine = new Java2Html();
engine.parseArguments(args);
// Turn on logging
Logger.getLogger("org.jcoderz.phoenix.report").setLevel(Level.FINEST);
engine.process();
}
/**
* Returns the string "odd" or "even", depending on the number given.
* @param number the number to check if it'S odd or even.
* @return the string "odd" if the given number is odd, "even"
* otherwise.
*/
public static String toOddEvenString (int number)
{
return PATTERN[number % PATTERN_SIZE];
}
/**
* Set the name of the package that should be treated as project "root"
* package.
* @param packageBase the project base package name.
*/
public void setPackageBase (String packageBase)
{
mPackageBase = packageBase;
logger.config("Package base set to '" + mPackageBase + "'.");
}
/**
* Set the timestamp when the report has been initiated.
*
* @param timestamp the report creation timestamp.
*/
public void setTimestamp (String timestamp)
{
mTimestamp = timestamp;
logger.config("Timestamp set to '" + mTimestamp + "'.");
}
/**
* Sets the flag if coverage data is available and should be taken
* into account.
* @param coverageDataAvailable true, if coverage data is available and
* should be taken into account.
*/
public void setCoverageData (boolean coverageDataAvailable)
{
mCoverageData = coverageDataAvailable;
logger.config("Coverage data set to '" + mCoverageData + "'.");
}
/**
* Sets the css file to be used can be relative to report path or
* absolute.
* @param style the css file to be used can be relative to report path or
* absolute.
*/
public void setStyle (String style)
{
mStyle = style;
logger.config("Style set to '" + mStyle + "'.");
}
/**
* Base url to the wiki to use.
* Finding pages link to a wiki page combined of this url and the
* finding type.
* @param wikiBase url to the wiki to use.
*/
public void setWikiBase (String wikiBase)
{
logger.config("Wiki base set to '" + wikiBase + "'.");
System.getProperties().setProperty(WIKI_BASE_PROPERTY,
wikiBase);
}
/**
* Base path (url) to the cvs repository used to create links to
* the cvs.
* @param cvsBase url that points to a web cvs of the project.
*/
public void setCvsBase (String cvsBase)
{
mWebVcBase = cvsBase;
logger.config("CVS base set to '" + mWebVcBase + "'.");
}
/**
* Suffix to be added at the end of web vc links.
* @param suffix String, to be added at the end of web vc links.
*/
public void setCvsSuffix (String suffix)
{
mWebVcSuffix = suffix;
logger.config("CVS suffix set to '" + suffix + "'.");
}
/**
* Returns the name of the project.
* @return the name of the project.
*/
public String getProjectName ()
{
return mProjectName;
}
/**
* Sets the name of the project used as readable string at several
* places in the report.
* @param name the name of the project used as readable string at several
* places in the report.
*/
public void setProjectName (String name)
{
Assert.notNull(name, "name");
mProjectName = name;
logger.config("Project name set to '" + getProjectName() + "'.");
}
/**
* Sets the tab with to be used when calculating the position in the
* current line.
* @param tabWidth the tab width to assume in input files.
*/
public void setTabwidth (String tabWidth)
{
Assert.notNull(tabWidth, "width");
mTabWidth = Integer.parseInt(tabWidth);
logger.config("Source tab width set to '" + mTabWidth + "'.");
}
/**
* The log level to be used when processing the input.
* Level must be parseable by {@link Level#parse(String)}.
* @param loglevel the log level to set.
*/
public void setLoglevel (String loglevel)
{
mLogLevel = Level.parse(loglevel);
LoggingUtils.setGlobalHandlerLogLevel(Level.ALL);
logger.fine("Setting log level: " + mLogLevel);
logger.setLevel(mLogLevel);
}
/**
* Sets the char-set used for the source files.
* @param charset The char-set in which the source files are expected.
*/
public void setSourceCharset (Charset charset)
{
Assert.notNull(charset, "charset");
mCharSet = charset;
logger.config("Source charset set to '" + charset + "'.");
}
/**
* The base path where the source files can be found.
* @param file base path where the source files can be found.
* @throws IOException if access to the path fails.
*/
public void setProjectHome (java.io.File file) throws IOException
{
final java.io.File projectHomeFile = file.getCanonicalFile();
mProjectHome = projectHomeFile.getCanonicalPath();
if (!projectHomeFile.isDirectory())
{
throw new RuntimeException("'projectHome' must be a directory '"
+ projectHomeFile + "'.");
}
logger.config("Using project home " + mProjectHome + ".");
}
/**
* The input file containing the jcoderz report.
* @param file input file containing the jcoderz report.
* @throws IOException if access to the file fails.
*/
public void setInputFile (java.io.File file) throws IOException
{
mInputData = file.getCanonicalFile();
if (!mInputData.canRead())
{
throw new RuntimeException("Can not read report file '"
+ mInputData + "'.");
}
logger.config("Using report file " + mInputData + ".");
}
/**
* The output directory where the report should be written to.
* If the directory does not exist it is created.
* @param dir the output directory where the report should be written to.
* @throws IOException if access or creation of the directory fails.
*/
public void setOutDir (java.io.File dir) throws IOException
{
mOutDir = dir.getCanonicalFile();
if (!mOutDir.exists())
{
if (!mOutDir.mkdir())
{
throw new RuntimeException("Could not create 'outDir' '"
+ mOutDir + "'.");
}
}
if (!mOutDir.isDirectory())
{
throw new RuntimeException("'outDir' must be a directory '"
+ mOutDir + "'.");
}
logger.config("Using out dir " + mOutDir + ".");
}
/**
* Starts the actual generation process.
* @throws JAXBException if the xmp parsing fails.
* @throws IOException if a IO problem occurs.
*/
public void process ()
throws JAXBException, IOException
{
final JAXBContext jaxbContext
= JAXBContext.newInstance("org.jcoderz.phoenix.report.jaxb",
this.getClass().getClassLoader());
final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setValidating(true);
mReport = (Report) unmarshaller.unmarshal(mInputData);
mGlobalSummary = new FileSummary();
initialiteFindingTypes();
for (final org.jcoderz.phoenix.report.jaxb.File file
: (List<org.jcoderz.phoenix.report.jaxb.File>) mReport.getFile())
{
try
{
if (file.getName() != null)
{
java2html(new java.io.File(file.getName()), file);
}
else
{
mGlobalFindings.add(file);
}
}
catch (Exception ex)
{
if (file.getItem().isEmpty())
{
logger.log(Level.FINE,
"No report for file without items '" + file.getName()
+ "'.", ex);
}
else
{
logger.log(Level.SEVERE,
"Failed to generate report for '" + file.getName()
+ "'.", ex);
mGlobalFindings.add(file);
}
}
}
// create package summary
for (final List<FileSummary> pkg : mAllPackages.values())
{
createPackageSummary(pkg);
}
createFullSummary();
createFindingsSummary();
createPerFindingSummary();
createAgeSummary();
logger.fine("Charts.");
try
{
final StatisticCollector sc;
if (mPackageBase == null)
{
sc = new StatisticCollector(mReport, mOutDir, mTimestamp);
}
else
{
sc = new StatisticCollector(
mReport, mPackageBase, mOutDir, mTimestamp);
}
sc.createCharts();
}
// we already created the report. There is no way
// to flag this as finding, so log it as a error
catch (Exception ex)
{
logger.log(Level.SEVERE,
"Failed to create charts for report. "
+ ex.getMessage(), ex);
}
copyStylesheet();
copyIcons();
logger.fine("Done.");
}
private void initialiteFindingTypes ()
{
for (final org.jcoderz.phoenix.report.jaxb.File file
: (List<org.jcoderz.phoenix.report.jaxb.File>) mReport.getFile())
{
for (Item i : (List<Item>) file.getItem())
{
try
{
FindingType.initialize(i.getOrigin());
}
catch (Exception ex)
{
logger.log(Level.WARNING,
"Could not initialize finding type "
+ i.getFindingType());
}
}
}
}
private void createAgeSummary ()
throws IOException
{
final List<Item> items = new ArrayList<Item>();
for (final org.jcoderz.phoenix.report.jaxb.File file
: (List<org.jcoderz.phoenix.report.jaxb.File>) mReport.getFile())
{
for (Item i : (List<Item>) file.getItem())
{
if (i.isSetSince())
{
items.add(i);
mItemToFileMap.put(i, file);
}
}
}
Collections.sort(items,
Collections.reverseOrder(new ItemAgeComperator()));
renderAgePage(items, Date.FUTURE_DATE, ReportInterval.BUILD);
renderAgePage(items, Date.FUTURE_DATE, ReportInterval.DAY);
renderAgePage(items, Date.FUTURE_DATE, ReportInterval.WEEK);
final Date oldest
= renderAgePage(items, Date.FUTURE_DATE, ReportInterval.MONTH);
renderAgePage(items, oldest, ReportInterval.OLD);
}
private void copyIcons ()
throws IOException
{
// create images sub-folder
final File outDir = new File(mOutDir, "images");
FileUtils.mkdirs(outDir);
for (int i = 0; i < Severity.VALUES.size(); i++)
{
final Severity s = Severity.fromInt(i);
if (s.equals(Severity.COVERAGE))
{
continue;
}
copyImage(outDir, "icon_" + s.toString() + ".gif");
copyImage(outDir, "bg-" + s.toString() + ".gif");
}
}
private void copyImage (final File outDir, final String name)
{
final InputStream in
= this.getClass().getResourceAsStream(name);
try
{
if (in != null)
{
copyResource(in, name, outDir);
}
else
{
logger.warning(
"Could not find resource '" + name + "'!");
}
}
finally
{
IoUtil.close(in);
}
}
private void copyResource (InputStream in, String resource, File outDir)
{
// Copy it to the output folder
OutputStream out = null;
try
{
out = new FileOutputStream(new File(outDir, resource));
FileUtils.copy(in, out);
}
catch (FileNotFoundException ex)
{
throw new RuntimeException("Can not find output folder '"
+ mOutDir + "'.", ex);
}
catch (IOException ex)
{
throw new RuntimeException("Could not copy resource '"
+ resource + "'.", ex);
}
finally
{
IoUtil.close(out);
}
}
private void copyStylesheet ()
{
// 1. Try to read the stylesheet from the jar (default stylesheet)
// 2. Try to open it from a user-defined location
// 3. Use the default one if the user-defined is not found
InputStream in = this.getClass().getResourceAsStream(mStyle);
try
{
if (in == null)
{
try
{
final File style = new File(mStyle);
in = new FileInputStream(style);
}
catch (FileNotFoundException ex)
{
IoUtil.close(in);
in = this.getClass().getResourceAsStream(DEFAULT_STYLESHEET);
if (in == null)
{
throw new RuntimeException("Can not find stylesheet file '"
+ mStyle + "'.", ex);
}
}
}
copyResource(in, DEFAULT_STYLESHEET, mOutDir);
}
finally
{
IoUtil.close(in);
}
}
private void parseArguments (String[] args)
{
try
{
for (int i = 0; i < args.length; )
{
if ("-outDir".equals(args[i]))
{
setOutDir(new java.io.File(args[i + 1]));
}
else if ("-report".equals(args[i]))
{
setInputFile(new java.io.File(args[i + 1]));
}
else if ("-projectHome".equals(args[i]))
{
setProjectHome(new java.io.File(args[i + 1]));
}
else if ("-projectName".equals(args[i]))
{
setProjectName(args[i + 1]);
}
else if ("-cvsBase".equals(args[i]))
{
setCvsBase(args[i + 1]);
}
else if ("-cvsSuffix".equals(args[i]))
{
setCvsSuffix(args[i + 1]);
}
else if ("-timestamp".equals(args[i]))
{
setTimestamp(args[i + 1]);
}
else if ("-wikiBase".equals(args[i]))
{
setWikiBase(args[i + 1]);
}
else if ("-reportStyle".equals(args[i]))
{
setStyle(args[i + 1]);
}
else if ("-noCoverage".equals(args[i]))
{
setCoverageData(false);
i -= 1;
}
else if ("-sourceEncoding".equals(args[i]))
{
setSourceCharset(Charset.forName(args[i + 1]));
}
else if ("-packageBase".equals(args[i]))
{
setPackageBase(args[i + 1]);
}
else if ("-loglevel".equals(args[i]))
{
setLoglevel(args[i + 1]);
}
else if ("-tabwidth".equals(args[i]))
{
setTabwidth(args[i + 1]);
}
else
{
throw new IllegalArgumentException(
"Invalid argument '" + args[i] + "'");
}
i += 1 /* command */ + 1 /* argument */;
}
}
catch (IndexOutOfBoundsException e)
{
final IllegalArgumentException ex
= new IllegalArgumentException("Missing value for "
+ args[args.length - 1]);
ex.initCause(e);
throw ex;
}
catch (Exception e)
{
final IllegalArgumentException ex = new IllegalArgumentException(
"Problem with arument value for " + args[args.length - 1]
+ "Argument line was " + ArraysUtil.toString(args));
ex.initCause(e);
throw ex;
}
}
/**
*
*/
private void createPerFindingSummary () throws IOException
{
for (final FindingsSummary.FindingSummary summary
: FindingsSummary.getFindingsSummary().getFindings().values())
{
final String filename = summary.createFindingDetailFilename();
final BufferedWriter out = openWriter(filename);
try
{
htmlHeader(out, "Finding-" + summary.getFindingType().getSymbol()
+ "-report " + mProjectName, "");
summary.createFindingTypeContent(out);
out.write("</body></html>");
}
finally
{
IoUtil.close(out);
}
}
}
private void createFindingsSummary () throws IOException
{
final BufferedWriter out = openWriter("findings.html");
try
{
htmlHeader(out, "Finding report " + mProjectName, "");
out.write("<h1><a href='index.html'>View by Classes</a></h1>");
out.write("<h1><a href='age-Build.html'>View by Age per Build</a> "
+ "<a href='age-Day.html'> Day</a> "
+ "<a href='age-Week.html'> Week</a> "
+ "<a href='age-Month.html'> Month</a> "
+ "<a href='age-Old.html'> Old</a></h1>");
out.write("<h1>Findings - Overview</h1>");
createNewFindingsList(out);
createOldFindingsList(out);
FindingsSummary.createOverallContent(out);
out.write("</body></html>");
}
finally
{
IoUtil.close(out);
}
}
private void createNewFindingsList (BufferedWriter out)
throws IOException
{
int row = 0;
for (org.jcoderz.phoenix.report.jaxb.File file
: (List<org.jcoderz.phoenix.report.jaxb.File>) mReport.getFile())
{
for (Item item : (List<Item>) file.getItem())
{
if (item.isNew())
{
if (row == 0)
{
openTable(out, "New");
}
createRow(out, file, item, row);
row++;
}
}
}
if (row > 0)
{
closeTable(out);
}
}
private void createOldFindingsList (BufferedWriter out)
throws IOException
{
int row = 0;
for (org.jcoderz.phoenix.report.jaxb.File file
: (List<org.jcoderz.phoenix.report.jaxb.File>) mReport.getFile())
{
for (Item item : (List<Item>) file.getItem())
{
if (item.isOld())
{
if (row == 0)
{
openTable(out, "Fixed");
}
createRow(out, file, item, row);
row++;
}
}
}
if (row > 0)
{
closeTable(out);
}
}
private void createRow (Writer bw,
org.jcoderz.phoenix.report.jaxb.File file, Item item, int rowCounter)
throws IOException
{
// Don't be to colorful, use the OK coloring
// for all entries.
bw.write("<tr class='findings-");
bw.write(Java2Html.toOddEvenString(rowCounter));
bw.write("row'><td class='findings-image'>");
appendSeverityImage(bw, item, "");
bw.write("</td><td width='100%' class='findings-data'>");
bw.write("<a href='");
bw.write(createReportLink(file));
bw.write("#LINE");
bw.write(String.valueOf(item.getLine()));
bw.write("'>");
bw.write(XmlUtil.escape(file.getClassname()));
bw.write(": ");
appendItemMessage(bw, item);
bw.write("</a></td><td>");
if (item.isSetSince())
{
bw.write(item.getSince().toDateString());
}
bw.write("</td></tr>\n");
}
private void openTable (Writer writer, String string)
throws IOException
{
writer.write("<h2 class='severity-header'>");
writer.write(string);
writer.write(" Findings</h2>");
writer.write("<table width='95%' cellpadding='2' cellspacing='0' "
+ "border='0'>");
}
private void closeTable (Writer bw)
throws IOException
{
bw.append("</table>");
}
/**
* converts a java source to HTML
* with syntax highlighting for the
* comments, keywords, strings and chars
*/
private void java2html (java.io.File inFile,
org.jcoderz.phoenix.report.jaxb.File data)
{
mCurrentFindings.clear();
mHandledFindings.clear();
mFindingsInFile.clear();
mFindingsInCurrentLine.clear();
mActiveItems.clear();
mCurrentFindings.addAll(data.getItem());
fillFindingsInFile(data);
logger.finest("Processing file " + inFile);
BufferedWriter bw = null;
String file = null;
try
{
mPackage = data.getPackage();
if (StringUtil.isEmptyOrNull(mPackage))
{
mPackage = UNNAMED_PACKAGE_NAME;
}
mClassname = data.getClassname();
// If no class name is reported take the filename.
if (StringUtil.isEmptyOrNull(mClassname))
{
mClassname = inFile.getName();
}
final String subdir = mPackage.replaceAll("\\.", "/");
final java.io.File dir = new java.io.File(mOutDir, subdir);
FileUtils.mkdirs(dir);
bw = openWriter(dir, mClassname + ".html");
final Syntax src = new Syntax(inFile, mCharSet, mTabWidth);
final FileSummary summary
= createFileSummary(src.getNumberOfLines(), subdir);
addSummary(summary);
file = mPackage + "." + mClassname;
htmlHeader(bw, mClassname, mPackage);
bw.write("<h1><a href='");
bw.write(relativeRoot(mPackage));
bw.write("'>Project Report: ");
bw.write(mProjectName);
bw.write("</a></h1>" + NEWLINE);
bw.write("<h2><a href ='index.html'>Packagesummary ");
bw.write(mPackage);
bw.write("</a></h2>" + NEWLINE);
final String cvsLink = getCvsLink(inFile.getAbsolutePath());
if (cvsLink != null)
{
bw.write("<h3><a href='" + cvsLink
+ "' class='cvs' title='cvs version'>" + file
+ "</a></h3>" + NEWLINE);
}
else
{
bw.write("<h3>" + file + "</h3>" + NEWLINE);
}
// create header!!!!
bw.write("<table border='0' cellpadding='2' cellspacing='0' "
+ "width='95%'>");
bw.write("<thead><tr><th>Line</th><th>Hits</th><th>Note</th>"
+ "<th class='remainder'>Source</th></tr></thead>");
bw.write("<tbody>");
// PASS 2
final int lastLine = src.getNumberOfLines();
for (int currentLine = 1; currentLine <= lastLine; currentLine++)
{
bw.write("<tr class='"
+ errorLevel(currentLine)
+ Java2Html.toOddEvenString(currentLine) + "'>");
bw.write("<td align='right' class='lineno");
final boolean isLast = currentLine == lastLine;
appendIf(bw, isLast, LAST_MARKER);
bw.write("'><a name='LINE" + currentLine + "' />");
bw.write(String.valueOf(currentLine));
bw.write("</td>");
hitsCell(bw, String.valueOf(getHits(currentLine)), isLast);
bw.write("<td class='note");
appendIf(bw, isLast, LAST_MARKER);
bw.write("'>");
bw.write(getIcons(currentLine));
bw.write("</td><td class='code-");
bw.write(Java2Html.toOddEvenString(currentLine));
appendIf(bw, isLast, LAST_MARKER);
bw.write("'>");
createCodeLine(bw, src);
bw.write("</td></tr>\n");
}
bw.write("</tbody>");
bw.write("</table>\n");
// findings table
bw.write("<h2 class='findings-header'>Findings in this File</h2>");
bw.write("<table width='95%' cellpadding='0' cellspacing='0' "
+ "border='0'>\n");
int rowCounter = 0;
final String relativeRoot = relativeRoot(mPackage, "");
int pos = mHandledFindings.size();
// findings with no line number or uncovered jet
for (final List<Item> lineFindings : mFindingsInFile.values())
{
for (final Item item : lineFindings)
{
if (!Origin.COVERAGE.equals(item.getOrigin()))
{
pos++;
rowCounter++;
bw.write("<tr class='findings-");
bw.write(Java2Html.toOddEvenString(rowCounter));
bw.write("row'>\n");
bw.write(" <td class='findings-image'>\n");
appendSeverityImage(bw, item, relativeRoot);
bw.write(" </td>\n");
bw.write(" <td class='findings-id'>\n");
bw.write(" <a name='FINDING" + pos + "' />\n");
bw.write(" (" + pos + ")\n");
bw.write(" </td>\n");
bw.write(" <td></td><td></td><td></td>\n"); // line number
bw.write(" <td width='100%' class='findings-data'>\n");
appendItemMessage(bw, item);
bw.write(" </td>\n");
bw.write("</tr>\n");
}
}
}
// Findings as marked in the code.
pos = 0;
for (final Item item : mHandledFindings)
{
pos++;
rowCounter++;
final String link = "#LINE" + item.getLine();
bw.write("<tr class='findings-");
bw.write(Java2Html.toOddEvenString(rowCounter));
bw.write("row'>\n");
bw.write(" <td class='findings-image'>\n");
appendSeverityImage(bw, item, relativeRoot);
bw.write(" </td>\n");
bw.write(" <td class='findings-id'>\n");
bw.write(" <a name='FINDING" + pos + "' />\n");
bw.write(" <a href='" + link + "' title='" + item.getOrigin()
+ "' >\n");
bw.write(" (" + pos + ")\n");
bw.write(" </a>\n");
bw.write(" </td>\n");
bw.write(" <td class='findings-line-number' align='right'>\n");
bw.write(" <a href='" + link + "' >\n");
bw.write(String.valueOf(item.getLine()));
bw.write(" </a>\n");
bw.write(" </td>\n");
bw.write(" <td class='findings-line-number' align='center'>\n");
bw.write(" <a href='" + link + "' >:</a>\n");
bw.write(" </td>\n");
bw.write(" <td class='findings-line-number' align='left'>\n");
bw.write(" <a href='" + link + "' >\n");
bw.write(String.valueOf(item.getColumn()));
bw.write(" </a>\n");
bw.write(" </td>\n");
bw.write(" <td width='100%' class='findings-data'>\n");
bw.write(" <a href='" + link + "' >\n");
appendItemMessage(bw, item);
bw.write("\n");
bw.write(" </a>\n");
bw.write(" </td>\n");
bw.write("</tr>\n");
}
bw.write("</table>\n");
bw.write("\n</body>\n</html>");
}
catch (FileNotFoundException fnfe)
{
logger.log(Level.WARNING, "Source file '" + file + "' not found.",
fnfe);
}
catch (IOException ioe)
{
logger.log(Level.WARNING, "Problem with '" + file + "'.", ioe);
}
finally
{
IoUtil.close(bw);
}
}
private void appendItemMessage (Writer bw, final Item item)
throws IOException
{
if (item.isOld())
{
bw.write("Fixed: ");
}
if (item.isNew())
{
bw.write("New: ");
}
bw.write(XmlUtil.escape(item.getMessage()));
if (item.getSeverityReason() != null)
{
bw.write(' ');
bw.write(XmlUtil.escape(item.getSeverityReason()));
}
}
/**
* Generates a image related to the severity of the given Item.
* The image links back to the general finding page of the item.
* @param w the writer where to write the output to.
* @param item the item to be documented.
* @param root the relative path from the page generated to the root dir.
* @throws IOException if the datas could not be written to the given
* writer.
*/
private void appendSeverityImage (Writer w, Item item, String root)
throws IOException
{
w.write("<a href='");
w.write(root);
w.write(FindingsSummary.getFindingsSummary()
.getFindingSummary(item).createFindingDetailFilename());
w.write("'><img border='0' title='");
w.write(String.valueOf(item.getSeverity()));
w.write(" [");
w.write(String.valueOf(item.getOrigin()));
w.write("]' alt='");
w.write(item.getSeverity().toString().substring(0, 1));
w.write("' src='");
w.write(root);
w.write(getImage(item.getSeverity()));
w.write("' /></a>\n");
}
private String getImage (Severity severity)
{
return "images/icon_" + severity.toString() + ".gif";
}
private FileSummary createFileSummary (final int linesCount,
final String subdir)
{
// Create file summary info
final FileSummary summary = new FileSummary(mClassname, mPackage,
subdir + "/" + mClassname + ".html", linesCount, mCoverageData);
for (final Item item : mCurrentFindings)
{
FindingsSummary.addFinding(item, summary);
if (Origin.COVERAGE.equals(item.getOrigin()))
{
if (item.getCounter() != 0)
{
summary.addCoveredLine();
}
else
{
summary.addViolation(Severity.COVERAGE);
}
}
else
{
summary.addViolation(item.getSeverity());
}
}
return summary;
}
private void fillFindingsInFile (
org.jcoderz.phoenix.report.jaxb.File data)
{
for (final Item item : (List<Item>) data.getItem())
{
final int lineNumber = item.getLine();
List<Item> itemsInLine = mFindingsInFile.get(lineNumber);
if (itemsInLine == null)
{
itemsInLine = new ArrayList<Item>();
mFindingsInFile.put(lineNumber, itemsInLine);
}
itemsInLine.add(item);
}
}
/**
* Generates the stylesheet link for html output.
* @param packageName the package of the current generated file. Used
* to generate a relative link.
* @param style the style to use (relative to report path or absolute)
* @return the style link as to be placed in the head of the generated
* html file.
*/
private static String createStyle (String packageName, String style)
{
// TODO: use the default stylesheet if not explicitly specified
String result = "";
if (style != null)
{
final String styleLink;
if (style.indexOf("//") != -1)
{ // absolute style
styleLink = style;
}
else
{
styleLink = relativeRoot(packageName, style);
}
result = "<link rel='stylesheet' type='text/css' href='"
+ styleLink + "' />";
}
return result;
}
private Severity errorLevel (int line)
{
Severity severity = Severity.OK;
final Iterator<Item> active = mActiveItems.iterator();
while (active.hasNext())
{
final Item item = active.next();
if (item.getEndLine() < line)
{
active.remove();
}
else
{
severity = severity.max(item.getSeverity());
}
}
final Iterator<Item> items = findingsInLine(line);
while (items.hasNext())
{
final Item item = items.next();
if (item.getOrigin().equals(Origin.COVERAGE))
{
if (item.getCounter() == 0)
{
severity = severity.max(Severity.COVERAGE);
}
}
else
{
severity = severity.max(item.getSeverity());
if (item.getEndLine() > line)
{
mActiveItems.add(item);
}
}
}
return severity;
}
private Iterator<Item> findingsInLine (int line)
{
final List<Item> findingsInLine = mFindingsInFile.get(line);
final Iterator<Item> result;
if (findingsInLine == null)
{
result = EmptyIterator.EMPTY_ITERATOR;
}
else
{
result = findingsInLine.iterator();
}
return result;
}
private String getHits (int line)
{
String hits = " ";
final Iterator<Item> items = findingsInLine(line);
while (items.hasNext())
{
final Item item = items.next();
if (Origin.COVERAGE.equals(item.getOrigin()))
{
hits = String.valueOf(item.getCounter());
break;
}
}
return hits;
}
/**
* Fills the 'Note' column for the given line.
* @param line the line under inspection.
* @return the content to be put in the notes column for the given line.
*/
private String getIcons (int line)
{
final StringBuilder icons = new StringBuilder();
// collect relevant findings
final Iterator<Item> items = findingsInLine(line);
while (items.hasNext())
{
final Item item = items.next();
if (Origin.COVERAGE.equals(item.getOrigin()))
{
if (item.getCounter() == 0)
{
items.remove(); // will never see this again!
}
}
else if (item.getSeverity() == Severity.FILTERED
|| item.isOld())
{
// not listen with the code but in the global section below the
// code.
}
else
{
mHandledFindings.add(item);
// create the magic icon string with a hyperlink
mGetIconsStringBuffer.setLength(0);
mGetIconsStringBuffer.append("<a href='#FINDING");
mGetIconsStringBuffer.append(mHandledFindings.size());
mGetIconsStringBuffer.append("' title='");
mGetIconsStringBuffer.append(
XmlUtil.attributeEscape(item.getMessage()));
mGetIconsStringBuffer.append("'><span class='");
mGetIconsStringBuffer.append(item.getOrigin());
mGetIconsStringBuffer.append("note'>(");
mGetIconsStringBuffer.append(mHandledFindings.size());
mGetIconsStringBuffer.append(")</span></a>");
icons.append(mGetIconsStringBuffer);
if (item.isSetColumn()
&& (!item.isSetEndLine()
|| item.getEndLine() - line <= MAX_LINES_WITH_INLINE_MARK))
{
mFindingsInCurrentLine.add(item);
}
items.remove(); // item was handled fully
}
}
if (icons.length() == 0)
{
icons.append(" ");
}
return icons.toString();
}
/**
* Replaces leading whitespace by a none breakable html string
* (entity).
* Uses <code>mStringBuffer</code> as temporary string buffer.
* @param in The string to modify.
* @return The string with leading white spaces replaced.
*/
private String replaceLeadingSpaces (String in)
{
final String result;
if (in == null || in.length() == 0)
{
result = " ";
}
else if (in.charAt(0) == ' ' || in.charAt(0) == '\t')
{
mStringBuilder.setLength(0);
int i;
int pos = 0;
for (i = 0; i < in.length()
&& (in.charAt(i) == ' ' || in.charAt(i) == '\t'); i++)
{
if (in.charAt(i) == ' ')
{
mStringBuilder.append(" ");
pos++;
}
else if (in.charAt(i) == '\t')
{
mStringBuilder.append(" ");
pos++;
while (pos % mTabWidth != 0)
{
mStringBuilder.append(" ");
pos++;
}
}
}
mStringBuilder.append(in.substring(i));
result = mStringBuilder.toString();
}
else
{
result = in;
}
return result;
}
/**
* Adds the file summary to all summary lists.
*/
private void addSummary (FileSummary summary)
{
mAllFiles.add(summary);
List<FileSummary> packageList
= mAllPackages.get(summary.getPackage());
if (packageList == null)
{
packageList = new ArrayList<FileSummary>();
mAllPackages.put(summary.getPackage(), packageList);
}
packageList.add(summary);
FileSummary packageSummary
= mPackageSummary.get(summary.getPackage());
if (packageSummary == null)
{
packageSummary = new FileSummary(mPackage);
mPackageSummary.put(summary.getPackage(), packageSummary);
}
packageSummary.add(summary);
mGlobalSummary.add(summary);
}
private void createPackageSummary (List<FileSummary> pkg)
throws IOException
{
createPackageSummary(new FileSummary.SortByPackage(), pkg);
createPackageSummary(new FileSummary.SortByQuality(), pkg);
createPackageSummary(new FileSummary.SortByCoverage(), pkg);
}
private void createPackageSummary (Comparator<FileSummary> order,
List<FileSummary> pkg)
throws IOException
{
final String filename = fileNameForOrder(order);
final String packageName = pkg.get(0).getPackage();
final String subdir = packageName.replaceAll("\\.", "/");
final java.io.File dir = new java.io.File(mOutDir, subdir);
FileUtils.mkdirs(dir);
final BufferedWriter bw = openWriter(dir, filename);
try
{
htmlHeader(bw, packageName, packageName);
bw.write("<h1><a href='" + relativeRoot(packageName, filename)
+ "'>Project-Report "
+ mProjectName + "</a></h1>");
bw.write("<h2>Packagesummary " + packageName + "</h2>");
createClassListTable(bw, pkg, false, order);
bw.write("</body></html>");
}
finally
{
IoUtil.close(bw);
}
}
private void createFullSummary ()
throws IOException
{
createFullSummary(new FileSummary.SortByPackage());
createFullSummary(new FileSummary.SortByQuality());
createFullSummary(new FileSummary.SortByCoverage());
}
private void createFullSummary (Comparator<FileSummary> order)
throws IOException
{
final String filename = fileNameForOrder(order);
final BufferedWriter bw = openWriter(filename);
try
{
htmlHeader(bw, "Project Report " + mProjectName, "");
bw.write("<h1>Project Report " + mProjectName + "</h1>");
bw.write("<table border='0' cellpadding='2' cellspacing='0' "
+ "width='95%'>");
bw.write("<thead><tr><th>");
if (filename != SORT_BY_PACKAGE_INDEX)
{
bw.write("<a href='" + SORT_BY_PACKAGE_INDEX
+ "' title='Sort by name'>");
}
bw.write("Package");
if (filename != SORT_BY_PACKAGE_INDEX)
{
bw.write("</a>");
}
bw.write("</th><th>findings</th>");
bw.write("<th>files</th><th>lines</th>");
if (mCoverageData)
{
bw.write("<th>%</th><th>");
if (filename != SORT_BY_COVERAGE_INDEX)
{
bw.write("<a href='" + SORT_BY_COVERAGE_INDEX
+ "' title='Sort by coverage'>");
}
bw.write("Coverage");
if (filename != SORT_BY_COVERAGE_INDEX)
{
bw.write("</a>");
}
bw.write("</th>");
}
bw.write("<th>%</th><th class='remainder'>");
if (filename != SORT_BY_QUALITY_INDEX)
{
bw.write("<a href='" + SORT_BY_QUALITY_INDEX
+ "' title='Sort by quality'>");
}
bw.write("Quality");
if (filename != SORT_BY_QUALITY_INDEX)
{
bw.write("</a>");
}
bw.write("</th></tr></thead>");
bw.write("<tbody>");
bw.write(NEWLINE);
bw.write("<tr class='odd'><td class='classname" + LAST_MARKER + "'>");
bw.write("Overall summary");
bw.write("</td>");
hitsCell(bw, String.valueOf(mGlobalSummary.getNumberOfFindings()),
true);
hitsCell(bw, String.valueOf(mGlobalSummary.getNumberOfFiles()), true);
hitsCell(bw, String.valueOf(mGlobalSummary.getLinesOfCode()), true);
if (mCoverageData)
{
hitsCell(bw, String.valueOf(mGlobalSummary.getCoverageAsString()),
true);
bw.write("<td valign='middle' class='hits" + LAST_MARKER
+ "' width='100'>");
bw.write(mGlobalSummary.getCoverageBar());
bw.write("</td>");
}
hitsCell(bw, String.valueOf(mGlobalSummary.getQuality()) + "%", true);
bw.write("<td valign='middle' class='code" + LAST_MARKER
+ "' width='100'>");
bw.write(mGlobalSummary.getPercentBar());
bw.write("</td></tr>");
bw.write(NEWLINE);
bw.write("</tbody>");
bw.write("</table>");
bw.write("<h1><a href='findings.html'>View by Finding</a></h1>");
bw.write("<h1><a href='age-Build.html'>View by Age per Build</a> "
+ "<a href='age-Day.html'> Day</a> "
+ "<a href='age-Week.html'> Week</a> "
+ "<a href='age-Month.html'> Month</a> "
+ "<a href='age-Old.html'> Old</a></h1>");
bw.write("<h1>Packages</h1>");
bw.write("<table border='0' cellpadding='2' cellspacing='0' "
+ "width='95%'>");
bw.write("<thead><tr><th>Package</th>"
+ "<th>findings</th><th>files</th><th>lines</th>");
if (mCoverageData)
{
bw.write("<th>%</th><th>Coverage</th>");
}
bw.write("<th>%</th><th class='remainder'>Quality</th></tr></thead>");
bw.write("<tbody>");
bw.write(NEWLINE);
final Set<FileSummary> packages = new TreeSet<FileSummary>(order);
packages.addAll(mPackageSummary.values());
int pos = 0;
final Iterator<FileSummary> i = packages.iterator();
while (i.hasNext())
{
pos++;
final FileSummary pkg = i.next();
final boolean isLast = !i.hasNext();
appendPackageLink(bw, pkg, filename, pos, isLast);
}
bw.write("</tbody></table>\n");
// findings with no line number...
createUnassignedFindingsTable(bw);
bw.write("<h1>Java Files</h1>");
createClassListTable(bw, mAllFiles, true, order);
bw.write("</body></html>");
}
finally
{
IoUtil.close(bw);
}
}
private void createUnassignedFindingsTable (final BufferedWriter bw)
throws IOException
{
boolean tableOpened = false;
int row = 0;
for (final org.jcoderz.phoenix.report.jaxb.File file : mGlobalFindings)
{
for (final Item item : (List<Item>) file.getItem())
{
row++;
if (!tableOpened)
{
bw.write("<h1>Unassigned findings</h1>");
bw.write("<table border='0' cellpadding='0' "
+ "cellspacing='0' width='95%'>");
tableOpened = true;
}
bw.write("<tr class='");
bw.write(item.getSeverity().toString());
bw.write(Java2Html.toOddEvenString(row));
bw.write("'><td class='unassigned-origin'>");
bw.write(ObjectUtil.toStringOrEmpty(item.getOrigin()));
bw.write("</td><td class='unassigned-filename'>");
bw.write(cutPath(file.getName()));
bw.write("</td><td class='unassigned-data' width='100%'>");
bw.write(item.getMessage());
bw.write("</td></tr>");
FindingsSummary.addFinding(item, mGlobalSummary);
}
}
if (tableOpened)
{
bw.write("</table>");
}
}
private void appendPackageLink (final BufferedWriter bw,
final FileSummary pkg, final String filename, final int pos,
final boolean isLast)
throws IOException
{
final String name = pkg.getPackage();
final String subdir = name.replaceAll("\\.", "/");
bw.write("<tr class='" + Java2Html.toOddEvenString(pos)
+ "'><td class='classname");
appendIf(bw, isLast, LAST_MARKER);
bw.write("'><a href='" + subdir + "/" + filename + "'>");
bw.write(pkg.getPackage());
bw.write("</a></td>");
hitsCell(bw, String.valueOf(pkg.getNumberOfFindings()), isLast);
hitsCell(bw, String.valueOf(pkg.getNumberOfFiles()), isLast);
hitsCell(bw, String.valueOf(pkg.getLinesOfCode()), isLast);
if (mCoverageData)
{
hitsCell(bw, String.valueOf(pkg.getCoverageAsString()), isLast);
bw.write("<td valign='middle' class='hits");
appendIf(bw, isLast, LAST_MARKER);
bw.write("' width='100'>");
bw.write(pkg.getCoverageBar());
bw.write("</td>");
}
hitsCell(bw, String.valueOf(pkg.getQuality()) + "%", isLast);
bw.write("<td valign='middle' class='code");
appendIf(bw, isLast, LAST_MARKER);
bw.write("' width='100'>");
bw.write(pkg.getPercentBar());
bw.write("</td></tr>" + NEWLINE);
}
/**
* Writes a html header to the given output stream.
* @param bw the stream to use for output
* @param title the title of the page. Should be the package name for
* sub packages.
*/
private void htmlHeader (Writer bw, String title, String packageName)
throws IOException
{
bw.write("<?xml version='1.0' encoding='UTF-8'?>" + NEWLINE
+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
+ NEWLINE
+ "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
+ NEWLINE
+ "<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en'"
+ " lang='en'>" + NEWLINE
+ "<head>" + NEWLINE
+ "\t<title>");
bw.write(title);
bw.write("</title>" + NEWLINE
+ "\t<meta name='author' content='jCoderZ java2html' />" + NEWLINE);
bw.write(createStyle(packageName, mStyle));
bw.write(NEWLINE + "</head>" + NEWLINE + "<body>" + NEWLINE);
}
private Date renderAgePage (
List<Item> findings, Date startDate, ReportInterval periode)
throws IOException
{
final Writer bw
= openWriter(mOutDir, "age-" + periode.toString() + ".html");
htmlHeader(bw, "Finding in " + periode.toString(), "");
bw.write("<h1><a href='index.html'>View by Classes</a></h1>");
bw.write("<h1><a href='findings.html'>View by Finding</a></h1>");
bw.write("<h1><a href='age-Build.html'>View by Age per Build</a> "
+ "<a href='age-Day.html'> Day</a> "
+ "<a href='age-Week.html'> Week</a> "
+ "<a href='age-Month.html'> Month</a> "
+ "<a href='age-Old.html'> Old</a></h1>");
if (ReportInterval.OLD != periode)
{
bw.write("<h1>Current Findings by " + periode.toString() + "</h1>");
}
else
{
bw.write("<h1>Old findings since " + startDate + "</h1>");
}
int numberOfSegments = 0;
Date iStart = null;
try
{
Date iEnd = null;
final List<Item> interval = new ArrayList<Item>();
for (Item i : findings)
{
if (iEnd == null)
{
if (i.getSince().before(startDate))
{
iStart = getPeriodStart(periode, i.getSince());
iEnd = getPeriodEnd(periode, i.getSince());
}
else
{
continue;
}
}
if (i.getSince().before(iStart))
{
Collections.sort(interval, new ItemSeverityComperator());
createSegment(bw, interval, iStart, iEnd);
interval.clear();
numberOfSegments++;
if (numberOfSegments > NUMBER_OF_AGE_SEGMENTS)
{
break;
}
iStart = getPeriodStart(periode, i.getSince());
iEnd = getPeriodEnd(periode, i.getSince());
}
interval.add(i);
}
if (!interval.isEmpty())
{
Collections.sort(interval, new ItemSeverityComperator());
createSegment(bw, interval, iStart, iEnd);
interval.clear();
}
}
finally
{
IoUtil.close(bw);
}
return iStart == null ? startDate : iStart;
}
private void createSegment (
Writer bw, List<Item> items, Date start, Date end)
throws IOException
{
if (!items.isEmpty())
{
openTable(bw, "Findings " + start
+ (start.equals(end) ? "" : (" - " + end))
+ " " + items.size());
}
int pos = 0;
for (final Item item : items)
{
createRow(bw, getFile(item), item, pos);
pos++;
}
if (!items.isEmpty())
{
closeTable(bw);
}
}
private org.jcoderz.phoenix.report.jaxb.File getFile (Item item)
{
return mItemToFileMap.get(item);
}
/*private*/ static Date getPeriodEnd (
ReportInterval periode, Date actualStart)
{
final Date result;
if (ReportInterval.BUILD == periode)
{
result = actualStart.plus(1);
}
else if (ReportInterval.DAY == periode)
{
final Calendar cal
= Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.setTimeInMillis(actualStart.getTime());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.DAY_OF_MONTH, 1);
result = new Date(cal.getTimeInMillis());
}
else if (ReportInterval.WEEK == periode)
{
final Calendar cal
= Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.setFirstDayOfWeek(Calendar.MONDAY);
cal.setTimeInMillis(actualStart.getTime());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
int dayOffset
= Calendar.MONDAY - cal.get(Calendar.DAY_OF_WEEK);
if (dayOffset <= 0)
{
dayOffset += cal.getMaximum(Calendar.DAY_OF_WEEK);
}
cal.add(Calendar.DAY_OF_MONTH, dayOffset);
result = new Date(cal.getTimeInMillis());
}
else if (ReportInterval.MONTH == periode)
{
final Calendar cal
= Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.setFirstDayOfWeek(Calendar.MONDAY);
cal.setTimeInMillis(actualStart.getTime());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.set(Calendar.DAY_OF_MONTH, 1);
cal.add(Calendar.MONTH, 1);
result = new Date(cal.getTimeInMillis());
}
else if (ReportInterval.OLD == periode)
{
result = actualStart;
}
else
{
Assert.fail("Unsupported value for ReportInterval " + periode);
result = null; // never reached
}
return result;
}
/*private*/ static Date getPeriodStart (ReportInterval periode, Date pos)
{
final Date result;
if (ReportInterval.BUILD == periode)
{
result = pos;
}
else if (ReportInterval.DAY == periode)
{
final Calendar cal
= Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.setTimeInMillis(pos.getTime());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
result = new Date(cal.getTimeInMillis());
}
else if (ReportInterval.WEEK == periode)
{
final Calendar cal
= Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.setFirstDayOfWeek(Calendar.MONDAY);
cal.setTimeInMillis(pos.getTime());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
int dayOffset
= Calendar.MONDAY - cal.get(Calendar.DAY_OF_WEEK);
if (dayOffset > 0)
{
dayOffset -= cal.getMaximum(Calendar.DAY_OF_WEEK);
}
cal.add(Calendar.DAY_OF_MONTH, dayOffset);
result = new Date(cal.getTimeInMillis());
}
else if (ReportInterval.MONTH == periode)
{
final Calendar cal
= Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.setFirstDayOfWeek(Calendar.MONDAY);
cal.setTimeInMillis(pos.getTime());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.set(Calendar.DAY_OF_MONTH, 1);
result = new Date(cal.getTimeInMillis());
}
else if (ReportInterval.OLD == periode)
{
result = Date.OLD_DATE;
}
else
{
Assert.fail("Unsupported value for ReportInterval " + periode);
result = null; // never reached
}
return result;
}
private static String relativeRoot (String currentPackage)
{
return relativeRoot(currentPackage, "index.html");
}
private static String relativeRoot (String currentPackage, String page)
{
final StringBuilder rootDir = new StringBuilder();
if (currentPackage.length() != 0)
{
rootDir.append("../");
}
for (int i = 0; i < currentPackage.length(); i++)
{
if (currentPackage.charAt(i) == '.')
{
rootDir.append("../");
}
}
rootDir.append(page);
return rootDir.toString().replaceAll("//", "/");
}
private String cutPath (String fileName)
{
String result = ObjectUtil.toStringOrEmpty(fileName);
if (fileName != null && fileName.toLowerCase(Constants.SYSTEM_LOCALE)
.startsWith(mProjectHome.toLowerCase(Constants.SYSTEM_LOCALE)))
{
result = fileName.substring(mProjectHome.length());
}
return result;
}
private String getCvsLink (String absFile)
{
String result;
if (mWebVcBase == null || mProjectHome == null)
{
result = null;
}
else if (absFile.toLowerCase(Constants.SYSTEM_LOCALE)
.startsWith(mProjectHome.toLowerCase(Constants.SYSTEM_LOCALE)))
{
result = absFile.substring(mProjectHome.length());
}
else
{
final String absPath
= (new java.io.File(absFile)).getAbsolutePath();
if (absPath.toLowerCase(Constants.SYSTEM_LOCALE).startsWith(
mProjectHome.toLowerCase(Constants.SYSTEM_LOCALE)))
{
result = absPath.substring(mProjectHome.length());
}
else
{
result = null;
}
}
if (result != null)
{
result = mWebVcBase + result + mWebVcSuffix;
result = result.replaceAll("\\\\", "/");
}
return result;
}
/**
* Create the marked html output for the current line.
* @param out the writer to generate the output to.
* @param src the source to read the line data from.
* @throws IOException if IO operations fail writing to the
* given writer.
*/
private void createCodeLine (Writer out, Syntax src)
throws IOException
{
String text = src.nextToken();
String lastTokenType = null;
Item lastFinding = null;
boolean isFirst = true;
while (null != src.getCurrentTokenType())
{
final Item currentFinding
= getCurrentFinding(text, src.getCurrentLineNumber(),
src.getCurrentLinePos(), src.getCurrentTokenLength());
if (currentFinding != lastFinding)
{
if (lastTokenType != null)
{
out.write("</span>"); // close token type
lastTokenType = null;
}
if (lastFinding != null)
{
out.write("</span>"); // close last finding
}
if (currentFinding != null)
{
out.write("<span class='finding-");
out.write(currentFinding.getSeverity().toString());
out.write("' title='");
out.write(
XmlUtil.attributeEscape(currentFinding.getMessage()));
out.write("'>");
}
lastFinding = currentFinding;
}
final String tokenType = src.getCurrentTokenType();
if (!tokenType.equals(lastTokenType))
{
if (lastTokenType != null)
{
out.write("</span>");
isFirst = false;
}
out.write("<span class='code-");
out.write(tokenType);
out.write("'>");
}
String xml = XmlUtil.escape(text);
if (isFirst)
{
xml = replaceLeadingSpaces(xml);
}
out.write(xml);
lastTokenType = tokenType;
text = src.nextToken();
}
if (lastTokenType != null)
{
out.write("</span>");
}
if (lastFinding != null)
{
out.write("</span>");
}
}
private Item getCurrentFinding (
String text, int currentLineNumber, int currentLinePos, int tokenLen)
{
Item theFinding = null;
final Iterator<Item> i = mFindingsInCurrentLine.iterator();
while (i.hasNext())
{
final Item finding = i.next();
if (finding.getEndLine() < currentLineNumber
&& finding.getLine() < currentLineNumber)
{
i.remove();
continue;
}
final int start =
finding.getLine() == currentLineNumber
? finding.getColumn() : 0;
int end =
finding.getEndLine() == currentLineNumber
? finding.getEndColumn() : ABSOLUTE_END_OF_LINE;
if (finding.getEndLine() == 0)
{
end = finding.getEndColumn();
}
// TODO Add code to find pos by symbol!
// finding at current pos
if ((start == currentLinePos && end == 0)
// or in range that contains the current pos
|| (start <= currentLinePos && end >= currentLinePos)
// or inside the token
|| (start >= currentLinePos
&& start < currentLinePos + tokenLen))
{
if (theFinding == null
|| finding.getSeverity().compareTo(
theFinding.getSeverity()) > 0)
{
theFinding = finding;
}
}
}
return theFinding;
}
/**
* Creates a list of all classes as html table and appends it to the
* given bw.
*/
private void createClassListTable (BufferedWriter bw,
Collection<FileSummary> files, boolean fullPackageNames,
Comparator<FileSummary> order)
throws IOException
{
// Do not create a table if no classes are here.
if (!files.isEmpty())
{
final String filename = fileNameForOrder(order);
final FileSummary[] summaries =
files.toArray(new FileSummary[files.size()]);
Arrays.sort(summaries, order);
bw.write("<table border='0' cellpadding='2' cellspacing='0' "
+ "width='95%'>");
bw.write("<thead><tr><th>");
if (filename != SORT_BY_PACKAGE_INDEX)
{
bw.write("<a href='");
bw.write(SORT_BY_PACKAGE_INDEX);
bw.write("' title='Sort by name'>");
}
bw.write("Classfile");
if (filename != SORT_BY_PACKAGE_INDEX)
{
bw.write("</a>");
}
bw.write("</th><th>findings</th><th>lines</th>");
if (mCoverageData)
{
bw.write("<th>%</th><th>");
if (filename != SORT_BY_COVERAGE_INDEX)
{
bw.write("<a href='");
bw.write(SORT_BY_COVERAGE_INDEX);
bw.write("' title='Sort by coverage'>");
}
bw.write("Coverage");
if (filename != SORT_BY_COVERAGE_INDEX)
{
bw.write("</a>");
}
bw.write("</th>");
}
bw.write("<th>%</th><th class='remainder'>");
if (filename != SORT_BY_QUALITY_INDEX)
{
bw.write("<a href='");
bw.write(SORT_BY_QUALITY_INDEX);
bw.write("' title='Sort by quality'>");
}
bw.write("Quality");
if (filename != SORT_BY_QUALITY_INDEX)
{
bw.write("</a>");
}
bw.write("</th></tr></thead>");
bw.write("<tbody>");
bw.write(NEWLINE);
int pos = 0;
final Iterator<FileSummary> i = Arrays.asList(summaries).iterator();
while (i.hasNext())
{
pos++;
final FileSummary file = i.next();
final boolean isLast = !i.hasNext();
appendClassLink(bw, file, fullPackageNames, pos, isLast);
}
bw.write("</tbody>");
bw.write("</table>");
}
else
{
bw.write("EMPTY!?");
}
}
private void appendClassLink (BufferedWriter bw, final FileSummary file,
boolean fullPackageNames, int pos, final boolean isLast)
throws IOException
{
final String name;
final String link;
if (fullPackageNames)
{
name = file.getPackage() + '.' + file.getClassName();
link = file.getPackage().replaceAll("\\.", "/") + "/"
+ file.getClassName() + ".html";
}
else
{
name = file.getClassName();
link = file.getClassName() + ".html";
}
bw.write("<tr class='");
bw.write(Java2Html.toOddEvenString(pos));
bw.write("'><td class='classname");
appendIf(bw, isLast, LAST_MARKER);
bw.write("'><a href='");
bw.write(link);
bw.write("'>");
bw.write(name);
bw.write("</a></td>");
hitsCell(bw, String.valueOf(file.getNumberOfFindings()), isLast);
hitsCell(bw, String.valueOf(file.getLinesOfCode()), isLast);
if (mCoverageData)
{
hitsCell(bw, file.getCoverageAsString(), isLast);
bw.write("<td valign='middle' class='hits");
appendIf(bw, isLast, LAST_MARKER);
bw.write("' width='100'>");
bw.write(file.getCoverageBar());
bw.write("</td>");
}
hitsCell(bw, String.valueOf(file.getQuality()) + "%", isLast);
bw.write("<td valign='middle' class='code");
appendIf(bw, isLast, LAST_MARKER);
bw.write("' width='100'>");
bw.write(file.getPercentBar());
bw.write("</td></tr>");
bw.write(NEWLINE);
}
private static String fileNameForOrder (Comparator<FileSummary> order)
{
String filename;
if (order instanceof FileSummary.SortByPackage)
{
filename = SORT_BY_PACKAGE_INDEX;
}
else if (order instanceof FileSummary.SortByQuality)
{
filename = SORT_BY_QUALITY_INDEX;
}
else if (order instanceof FileSummary.SortByCoverage)
{
filename = SORT_BY_COVERAGE_INDEX;
}
else
{
throw new RuntimeException("Order not expected " + order);
}
return filename;
}
/**
* Generates the content (including surounding elements) of a cell
* of hits type.
* @param bw the writer to write to.
* @param content the cell contend
* @param isLast true if this sell is in the last row.
* @throws IOException if the write fails.
*/
private static void hitsCell (BufferedWriter bw,
String content, boolean isLast)
throws IOException
{
bw.write("<td valign='middle' class='hits");
appendIf(bw, isLast, LAST_MARKER);
bw.write("'>");
bw.write(content);
bw.write("</td>");
}
private static void appendIf (BufferedWriter bw, boolean condition,
String str)
throws IOException
{
if (condition)
{
bw.write(str);
}
}
private BufferedWriter openWriter (String filename)
throws IOException
{
return openWriter(mOutDir, filename);
}
private BufferedWriter openWriter (File dir, String filename)
throws IOException
{
FileOutputStream fos = null;
OutputStreamWriter osw = null;
BufferedWriter result = null;
try
{
fos = new FileOutputStream(new java.io.File(dir, filename));
osw = new OutputStreamWriter(fos, Constants.ENCODING_UTF8);
result = new BufferedWriter(osw);
}
catch (RuntimeException ex)
{
IoUtil.close(result);
IoUtil.close(osw);
IoUtil.close(fos);
throw ex;
}
catch (IOException ex)
{
IoUtil.close(result);
IoUtil.close(osw);
IoUtil.close(fos);
throw ex;
}
return result;
}
private static String createReportLink (
org.jcoderz.phoenix.report.jaxb.File file)
{
final String pkg
= StringUtil.isEmptyOrNull(file.getPackage())
? UNNAMED_PACKAGE_NAME : file.getPackage();
String clazzName = ObjectUtil.toStringOrEmpty(file.getClassname());
// If no class name is reported take the filename.
if (StringUtil.isEmptyOrNull(clazzName))
{
if (!StringUtil.isEmptyOrNull(file.getName()))
{
clazzName = new File(file.getName()).getName();
}
else
{
clazzName = "";
}
}
final String subdir = pkg.replaceAll("\\.", "/");
return subdir + "/" + clazzName + ".html";
}
private static class ItemSeverityComperator
implements Comparator<Item>, Serializable
{
private static final long serialVersionUID = 1L;
public int compare (Item o1, Item o2)
{
int result = 0;
result = -(o1.getSeverity().compareTo(o2.getSeverity()));
if (result == 0)
{
result = o1.getSince().compareTo(o2.getSince());
}
if (result == 0)
{
if (o1.getLine() > o2.getLine())
{
result = 1;
}
else if (o1.getLine() < o2.getLine())
{
result = -1;
}
else
{
result = 0;
}
}
if (result == 0)
{
result = o1.getFindingType().compareTo(o2.getFindingType());
}
return result;
}
}
private static class ItemAgeComperator
implements Comparator<Item>, Serializable
{
private static final long serialVersionUID = 1L;
public int compare (Item o1, Item o2)
{
final Date since1
= o1.isSetSince() ? o1.getSince() : Date.OLD_DATE;
final Date since2
= o2.isSetSince() ? o2.getSince() : Date.OLD_DATE;
return since1.compareTo(since2);
}
}
}