/*
* $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.Serializable;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Comparator;
import java.util.Iterator;
import org.jcoderz.commons.util.Assert;
import org.jcoderz.commons.util.StringUtil;
import org.jcoderz.phoenix.report.jaxb.File;
import org.jcoderz.phoenix.report.jaxb.Item;
/**
* This class encapsulates all finding information collected
* for a file or a group of files.
*
* <p>This class also allows to perform the magic quality
* calculation for the data collected in the summary.</p>
*
* @author Andreas Mandel
*/
public final class FileSummary
implements Comparable<FileSummary>
{
/** Constant used for initial string buffer size. */
private static final int STRING_BUFFER_SIZE = 256;
/** Constant for percentage calculation 1% = 1 / MAX_PERCENTAGE. */
private static final int MAX_PERCENTAGE = 100;
private static final float MAX_PERCENTAGE_FLOAT = 100;
private final NumberFormat mCoveragePercantageFormatter =
new DecimalFormat("##0.00");
/** Counts the number of files added up in this summary. */
private int mFiles = 0;
/** Lines of code in the file. */
private int mLinesOfCode;
/**
* Lines of code in the file that contain coverage information
* that is not 0.
*/
private int mCoveredLinesOfCode;
/**
* Holds the number of violations for each severity level.
*/
private int[] mViolations = new int[Severity.VALUES.size()];
/**
* Percentage values for the violations.
* Data stored in here is only valid if <code>mPercentUpToDate</code>
* is true.
*/
private int[] mPercent = new int[Severity.VALUES.size()];
private boolean mPercentUpToDate = false;
private final String mClassName;
private final String mPackage;
private final String mDetailedFile;
private boolean mCoverageData;
/**
* Creates a new empty file summary object used to summarize
* findings for classes in all packages.
*/
public FileSummary ()
{
this ("Global Summary", "all", null, 0, false);
}
/**
* Creates a new empty file summary object used to summarize
* findings for classes in in the given package.
*
* @param packagename name of the package where this summary
* is used for.
*/
public FileSummary (String packagename)
{
this ("Package Summary", packagename, null, 0, false);
}
/**
* Creates a new empty file summary object used to summarize
* findings for the given class in the given package with
* link to the file and code information.
* @param className the name of the class (without package
* information).
* @param packagename the name of the package where the class
* resides in.
* @param reportfile the name of the file where the html report
* stored.
* @param linesOfCode the number of lines in the file.
* @param withCoverage true if coverage information is
* available.
*/
public FileSummary (String className, String packagename,
String reportfile, int linesOfCode, boolean withCoverage)
{
mClassName = className;
mPackage = packagename;
mDetailedFile = reportfile;
mLinesOfCode = linesOfCode;
mCoverageData = withCoverage;
}
/**
* Calculates the quality as percentage represented as float.
* @param loc total number of lines of code. This is also the maximum
* that might be returned by this method.
* @param violations the array holding the violations of the severity
* related to the position in the array. The elements of the
* array are NOT modified.
* @return the quality as percentage represented as float.
*/
public static float calculateQuality (int loc, int[] violations)
{
float quality = 0;
if (loc > 0)
{
quality = calcUnweightedQuality(loc, violations);
quality = (quality * MAX_PERCENTAGE) / loc;
}
return quality;
}
/**
* Calculates the unweighed quality points scored for the code.
* Maximum returned is <code>loc</code> the minimum is <code>0</code>.
* @param loc total number of lines of code. This is also the maximum
* that might be returned by this method.
* @param violations the array holding the violations of the severity
* related to the position in the array. The elements of the
* array are NOT modified.
* @return the unweighed quality score.
*/
private static int calcUnweightedQuality (int loc, int[] violations)
{
Assert.assertEquals(
"Violations array length must match number of severities.",
Severity.VALUES.size(), violations.length);
int quality = loc * Severity.PENALTY_SCALE; // lines of code
for (int i = 0; i < Severity.VALUES.size() && quality > 0; i++)
{
// not covered lines are bad this
// not files with no coverage test at all get no penalty here!
quality -= violations[i] * Severity.fromInt(i).getPenalty();
}
if (quality < 0)
{
quality = 0;
}
else
{
quality /= Severity.PENALTY_SCALE;
}
return quality;
}
/**
* Calculates the quality percentage scored for the code.
* Maximum returned is <code>100</code> the minimum is <code>0</code>.
* @param loc total number of lines of code. This is also the maximum
* that might be returned by this method.
* @param info number of info level findings.
* @param warning number of warning level findings.
* @param error number of error level findings.
* @param coverage number of coverage level findings.
* @param filtered number of filtered level findings.
* @param codestyle number of codestyle level findings.
* @param design number of design level findings.
* @param cpd number of cpd level findings.
* @return the unweighed quality score.
*/
public static float calculateQuality (int loc, int info, int warning,
int error, int coverage, int filtered, int codestyle, int design,
int cpd)
{
final int[] violations = new int[Severity.VALUES.size()];
violations[Severity.INFO.toInt()] = info;
violations[Severity.COVERAGE.toInt()] = coverage;
violations[Severity.WARNING.toInt()] = warning;
violations[Severity.ERROR.toInt()] = error;
violations[Severity.FILTERED.toInt()] = filtered;
violations[Severity.CODE_STYLE.toInt()] = codestyle;
violations[Severity.DESIGN.toInt()] = design;
violations[Severity.CPD.toInt()] = cpd;
return FileSummary.calculateQuality(loc, violations);
}
/** @return the name of the class (without package information). */
public String getClassName ()
{
return mClassName;
}
/** @return the name of the package. */
public String getPackage ()
{
return mPackage;
}
/** @return the number of files summarized in this file summary. */
public int getNumberOfFiles ()
{
return mFiles;
}
/** {@inheritDoc} */
public String toString ()
{
final StringBuilder result = new StringBuilder();
calcPercent();
result.append(getFullClassName());
result.append("{ LOC:");
result.append(mLinesOfCode);
result.append('(');
result.append(mViolations[Severity.OK.toInt()]);
result.append("%)");
final Iterator<Severity> i = Severity.VALUES.iterator();
while (i.hasNext())
{
final Severity s = i.next();
if (mViolations[s.toInt()] != 0)
{
result.append(", ");
result.append(s.toString());
result.append(':');
result.append(mViolations[s.toInt()]);
result.append('(');
result.append(mPercent[s.toInt()]);
result.append("%)");
}
}
result.append('}');
return result.toString();
}
/**
* Add the counters of an other FileSummary to this one.
* @param other the FileSummary be added.
*/
public void add (FileSummary other)
{
for (int i = 0; i < mViolations.length; i++)
{
mViolations[i] += other.mViolations[i];
}
mLinesOfCode += other.mLinesOfCode;
mCoveredLinesOfCode += other.mCoveredLinesOfCode;
mPercentUpToDate = false;
mFiles++;
if (mCoverageData || other.isWithCoverage()
|| mCoveredLinesOfCode > 0
|| getNotCoveredLinesOfCode() > 0)
{
mCoverageData = true;
}
}
/**
* Adds the counters from the given file to this summary.
* @param file the data to be added.
*/
public void add (File file)
{
mFiles++;
mLinesOfCode += file.getLoc();
final Iterator<Item> i = file.getItem().iterator();
while (i.hasNext())
{
final Item item = i.next();
final Severity severity = item.getSeverity();
addViolation(severity);
}
}
/**
* Returns true if this summary contains coverage data.
* @return true if this summary contains coverage data.
*/
public boolean isWithCoverage ()
{
return mCoverageData;
}
/** Increments the counter of covered lines. */
public void addCoveredLine ()
{
mPercentUpToDate = false;
mCoveredLinesOfCode++;
}
/**
* Increments the counter for the given severity in this summary.
* @param severity the severity of the counter to be incremented.
*/
public void addViolation (Severity severity)
{
Assert.notNull(severity, "severity");
mPercentUpToDate = false;
mViolations[severity.toInt()]++;
}
/**
* @return the full class name including package declaration.
*/
public String getFullClassName ()
{
final String fullClassName;
if (StringUtil.isEmptyOrNull(mPackage))
{
fullClassName = mClassName;
}
else
{
fullClassName = mPackage + "." + mClassName;
}
return fullClassName;
}
/** @return the report file associated to this FileSummary. */
public String getHtmlLink ()
{
return mDetailedFile;
}
/** @return the number of lines of code. */
public int getLinesOfCode ()
{
return mLinesOfCode;
}
/**
* Returns the magic quality as percentage int.
* The maximum quality code gets a score of 100. The lowest score
* possible is 0.
* @return the magic quality as percentage int (0-100).
*/
public int getQuality ()
{
int quality = 0;
if (mLinesOfCode > 0)
{
quality = calcUnweightedQuality(mLinesOfCode, mViolations);
quality = (quality * MAX_PERCENTAGE) / mLinesOfCode;
}
return quality;
}
/**
* Returns the magic quality as percentage float.
* The maximum quality code gets a score of 100. The lowest score
* possible is 0.
* @return the magic quality as percentage float (0.0-100.0).
*/
public float getQualityAsFloat ()
{
// might be we should cache the result?
return FileSummary.calculateQuality (mLinesOfCode, mViolations);
}
/**
* Generates a string containing xhtml code that renders to a
* percentage bar that can be used as component of a web page.
* @return a string containing xhtml.
*/
public String getPercentBar ()
{
calcPercent();
final StringBuilder sb = new StringBuilder(STRING_BUFFER_SIZE);
sb.append("<table width='100%' cellspacing='0' cellpadding='0' "
+ "summary='quality-bar'><tr valign='middle'>");
for (int i = Severity.OK.toInt(); i < Severity.MAX_SEVERITY_INT; i++)
{
if (mPercent[i] > 0)
{
sb.append("<td class='");
sb.append(Severity.fromInt(i).toString());
sb.append("' width='");
sb.append(mPercent[i]);
sb.append("%' height='10'></td>");
}
}
sb.append("</tr></table>");
return sb.toString();
}
/**
* Generates a string containing xhtml code that renders to a
* bar that can be used as component of a web page to represent
* the amount of covered code.
* @return a string containing xhtml.
*/
public String getCoverageBar ()
{
final int notCovered = MAX_PERCENTAGE - getCoverage();
final StringBuilder sb = new StringBuilder(STRING_BUFFER_SIZE);
sb.append("<table width='100%' cellspacing='0' cellpadding='0' "
+ "summary='coverage-bar'><tr valign='middle'>");
if (notCovered < MAX_PERCENTAGE)
{
sb.append("<td class='ok' width='");
sb.append(MAX_PERCENTAGE - notCovered);
sb.append("%' height='10'></td>");
}
if (notCovered != 0)
{
sb.append("<td class='error' width='");
sb.append(notCovered);
sb.append("%' height='10'></td></tr>");
}
sb.append("</tr></table>");
return sb.toString();
}
/**
* Returns the number of violations for the given severity.
* @param severity the severity to check.
* @return the number of violations for the given severity
*/
public int getViolations (Severity severity)
{
return mViolations[severity.toInt()];
}
/**
* Get the coverage percentage in double precision.
* @return the coverage percentage in double precision.
*/
public float getCoverageAsFloat ()
{
final float allLinesOfCode
= getNotCoveredLinesOfCode() + mCoveredLinesOfCode;
float result;
if (allLinesOfCode != 0)
{
result = mCoveredLinesOfCode / allLinesOfCode;
}
else if (getNotCoveredLinesOfCode() > 0)
{
result = 0;
}
else // no coverage at all (might be interface...
{
result = 1;
}
return result * MAX_PERCENTAGE_FLOAT;
}
/**
* Returns the coverage as user string.
* @return the coverage as user string.
*/
public String getCoverageAsString ()
{
return mCoveragePercantageFormatter.format(getCoverageAsFloat()) + "%";
}
/** @return the coverage percentage as int. */
public int getCoverage ()
{
final int notCoveredLinesOfCode = getNotCoveredLinesOfCode();
int notCovered;
if (mCoveredLinesOfCode != 0)
{
notCovered = (notCoveredLinesOfCode * MAX_PERCENTAGE)
/ (mCoveredLinesOfCode + notCoveredLinesOfCode);
if ((notCovered == 0) && (notCoveredLinesOfCode > 0))
{ // below 1% -> round up to 1%
notCovered = 1;
}
}
else if (notCoveredLinesOfCode > 0)
{
notCovered = MAX_PERCENTAGE;
}
else // no coverage at all (might be interface...
{
notCovered = 0;
}
return MAX_PERCENTAGE - notCovered;
}
/**
* @return the number of not covered lines of code.
*/
public int getNotCoveredLinesOfCode ()
{
return mViolations[Severity.COVERAGE.toInt()];
}
/**
* All findings that are between {@link Severity#INFO} and
* {@link Severity#ERROR} but not {@link Severity#COVERAGE}
* are counted.
* @return the number of violations summed up in this summary.
*/
public int getNumberOfFindings ()
{
int sum = 0;
for (int i = Severity.INFO.toInt(); i <= Severity.ERROR.toInt(); i++)
{
if (i != Severity.COVERAGE.toInt())
{
sum += mViolations[i];
}
}
return sum;
}
/** {@inheritDoc} */
public int compareTo (FileSummary o)
{
int result = 0;
if (mPackage != null)
{
result = mPackage.compareTo((o).mPackage);
}
if (result == 0)
{
if (getClassName() != null)
{
result = getClassName().compareTo(o.getClassName());
}
}
return result;
}
private void calcPercent ()
{
if (!mPercentUpToDate)
{
doCalcPercent();
}
}
private void doCalcPercent ()
{
int remainingPercentage = MAX_PERCENTAGE;
// errors
if (mLinesOfCode != 0)
{
for (int i = Severity.ERROR.toInt(); i > Severity.INFO.toInt();
i--)
{
int percent;
if (i == Severity.COVERAGE.toInt())
{
percent = calcPercentCoverage();
}
else
{
percent = calcPercentage(
mViolations[i] * Severity.fromInt(i).getPenalty(),
mLinesOfCode * Severity.PENALTY_SCALE);
}
// do not round to 0.
if (mViolations[i] > 0 && percent == 0)
{
percent = 1;
}
if (percent > remainingPercentage)
{
percent = remainingPercentage;
}
mPercent[i] = percent;
remainingPercentage -= percent;
}
}
else
{
for (int i = Severity.ERROR.toInt(); i > Severity.INFO.toInt(); i--)
{
mPercent[i] = 0;
}
}
mPercent[Severity.OK.toInt()] = remainingPercentage;
mPercentUpToDate = true;
}
/**
* Calculates the penalty percentage of the coverage tests.
*/
private int calcPercentCoverage ()
{
final int coverageViolationPercentage;
if (!mCoverageData)
{
coverageViolationPercentage = 0;
}
else
{
final int notCoveredLines
= getNotCoveredLinesOfCode();
coverageViolationPercentage = calcPercentage(
notCoveredLines * Severity.COVERAGE.getPenalty(),
Severity.PENALTY_SCALE
* (mCoveredLinesOfCode + notCoveredLines));
}
return coverageViolationPercentage;
}
private static int calcPercentage (int part, int all)
{
final int result;
if (all == 0)
{
result = 0;
}
else
{
result = part * MAX_PERCENTAGE / all;
}
return result;
}
/**
* Comparator that allows to sort the FileSummary by name of the package.
* @author Andreas Mandel
*/
static final class SortByPackage
implements Comparator<FileSummary>, Serializable
{
private static final long serialVersionUID = 2244367340241672131L;
public int compare (FileSummary o1, FileSummary o2)
{
return o1.compareTo(o2);
}
}
/**
* Comparator that allows to sort the FileSummary by quality.
* @author Andreas Mandel
*/
static final class SortByQuality
implements Comparator<FileSummary>, Serializable
{
private static final long serialVersionUID = 1718175789352629538L;
public int compare (FileSummary o1, FileSummary o2)
{
int result;
final float qualityA = o1.getQualityAsFloat();
final float qualityB = o2.getQualityAsFloat();
if (qualityA < qualityB)
{
result = -1;
}
else if (qualityA > qualityB)
{
result = 1;
}
else
{
result = o1.compareTo(o2);
}
return result;
}
}
/**
* Comparator that allows to sort the FileSummary by coverage.
* @author Andreas Mandel
*/
static final class SortByCoverage
implements Comparator<FileSummary>, Serializable
{
private static final long serialVersionUID = -4275903074787742250L;
public int compare (FileSummary a, FileSummary b)
{
final float coverA = a.getCoverageAsFloat();
final float coverB = b.getCoverageAsFloat();
final int result;
if (coverA < coverB)
{
result = -1;
}
else if (coverA > coverB)
{
result = 1;
}
else if (a.getNotCoveredLinesOfCode() > b.getNotCoveredLinesOfCode())
{
result = -1;
}
else if (a.getNotCoveredLinesOfCode() < b.getNotCoveredLinesOfCode())
{
result = 1;
}
else
{
result = a.compareTo(b);
}
return result;
}
}
}