/* * $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.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.jcoderz.commons.util.XmlUtil; import org.jcoderz.phoenix.report.jaxb.Item; /** * This class holds all findings, by type and file for the project. * * This class in NOT thread save in any way. * * @author Andreas Mandel */ final class FindingsSummary { /** Singleton type findings collector. */ private static FindingsSummary sFindingsSummary = new FindingsSummary(); private final Map<String, FindingSummary> mFindings = new HashMap<String, FindingSummary>(); private int mOverallCounter = 0; private FindingsSummary () { // singleton class only instantiated by the factory } /** * Utility method to get the Singleton. * @return the one and only findings summary object. */ public static FindingsSummary getFindingsSummary () { return sFindingsSummary; } /** * Generates a key unique for kind of the given finding. * @param finding item where to generate the key for. * @return a key unique for kind of the given finding. */ public static String getKeyForFinding (Item finding) { return finding.getFindingType() + "_" + finding.getSeverity().toString(); } /** * Generates a key unique for kind of the given finding type and * severity. * @param findingType the type to generate the key for. * @param severity the severity to generate the key for. * @return a key unique for kind of the given finding type and * severity. */ public static String getKeyForFinding (FindingType findingType, Severity severity) { return findingType.getSymbol() + "_" + severity.toString(); } /** * Adds the finding to the findings data structure. * All references and counters are updated. * @param finding the concrete item that was detected * @param file the FileSummary object of the detected finding. */ public static void addFinding (Item finding, FileSummary file) { getFindingsSummary().getFindingSummary(finding) .addFinding(finding, file); } /** * Provides access to all findings of the given type. * @param findingType the type of the finding. * @param severity the severity of the finding. * @return a FindingSummary of all findings of * the given FindingType, might be null if * no such finding exists. */ public FindingSummary getFindingSummary (FindingType findingType, Severity severity) { return mFindings.get(getKeyForFinding(findingType, severity)); } /** * Returns the FindingSummary appropriate to hold findings of the * type of the given Item. * If no such summary exists yet, a new one is generated and * returned. * @param item the item where to return a summary for. * @return the FindingSummary appropriate to hold findings of the * type of the given Item. */ public FindingSummary getFindingSummary (Item item) { final String key = getKeyForFinding(item); // cast to make sure we get an exception once item.getFindingType // returns a real FindingType FindingSummary result = mFindings.get(key); if (result == null) { result = new FindingSummary(item); } return result; } /** * Returns the map mapping from the type/severity string to stored * FindingSummary objects. * The returned map is immutable. Stored objects MUST not be * modified. * @return the map mapping from the type/severity string to stored * FindingSummary objects. */ Map<String, FindingSummary> getFindings () { return Collections.unmodifiableMap(mFindings); } /** {@inheritDoc} */ public String toString () { return "[FindingsSummary: " + mFindings + "(" + mOverallCounter + ")]"; } /** * Generates a page that lists all findings, that links to the * detailed finding pages. The content is ordered by severity and * number of occurrences. * @param out the writer where to write the html data to. * @throws IOException if the data can not be written. */ static void createOverallContent (Writer out) throws IOException { final Collection<FindingSummary> colAllFindings = getFindingsSummary().getFindings().values(); final FindingSummary[] allFindings = colAllFindings.toArray( new FindingSummary[colAllFindings.size()]); Arrays.sort(allFindings); Severity currentSeverity = null; out.write("<table border='0' cellpadding='0' cellspacing='0' " + "width='95%' summary='Summary of all findings.'>"); int row = 0; for (final FindingSummary summary : allFindings) { if (summary.getSeverity() != currentSeverity) { out.write("<tr><td colspan='3' class='severityheader'>"); currentSeverity = summary.getSeverity(); out.write("<a name='" + currentSeverity.toString() + "'/>"); out.write("Severity: "); out.write(currentSeverity.toString()); out.write("\n</td></tr>"); row = 0; } row++; out.write("<tr class='" + currentSeverity + Java2Html.toOddEvenString(row) + "'>"); out.write("<td class='finding-counter'>"); out.write(String.valueOf(summary.getCounter())); out.write("</td>"); out.write("<td class='finding-origin'>"); out.write(summary.getOrigin().toString()); out.write("</td>"); out.write("<td class='finding-data' width='100%'>"); out.write("<a href='"); out.write(summary.createFindingDetailFilename()); out.write("' title='"); out.write(summary.getFindingType().getSymbol()); out.write("'>"); // if (summary.isFindingsHaveSameMessage() // && summary.getFindingMessage() != null) // { // out.write(summary.getFindingMessage()); // } // else { out.write(summary.getFindingType().getShortText()); } out.write("</a></td></tr>\n"); } out.write("</table>"); } /** * Holds all findings of a specific type. * @author Andreas Mandel */ final class FindingSummary implements Comparable<FindingSummary> { private final Map<String, FindingOccurrence> mOccurrences = new HashMap<String, FindingOccurrence>(); private final Severity mSeverity; private final Origin mOrigin; private int mCounter; private boolean mFindingsHaveSameMessage = true; private final String mFindingMessage; private final FindingType mFindingType; /** * Creates a new FindingSummary to collect findings similiar * to the given finding. * @param finding the reference Item for the types of findings * collected in this summary. */ public FindingSummary (Item finding) { final String key = getKeyForFinding(finding); mFindingType = FindingType.fromString(finding.getFindingType()); mSeverity = finding.getSeverity(); mOrigin = finding.getOrigin(); mFindingMessage = finding.getMessage(); mFindings.put(key, this); } /** * @return Returns the counter. */ public int getCounter () { return mCounter; } /** * @return Returns the origin of the findings. */ public Origin getOrigin () { return mOrigin; } /** * @return Returns the severity. */ public Severity getSeverity () { return mSeverity; } /** * @return Returns the findingMessage. */ public String getFindingMessage () { return mFindingMessage; } /** * @return Returns the findingsHaveSameMessage. */ public boolean isFindingsHaveSameMessage () { return mFindingsHaveSameMessage; } /** * @return Returns the finding type. */ public FindingType getFindingType () { return mFindingType; } /** * @return Returns the occurrences. */ public Map<String, FindingOccurrence> getOccurrences () { return Collections.unmodifiableMap(mOccurrences); } public FindingOccurrence getOccurrence (FileSummary fileSummary) { FindingOccurrence result = getOccurrence(fileSummary.getFullClassName()); if (result == null) { result = new FindingOccurrence(fileSummary); } return result; } public void addFinding (Item finding, FileSummary summary) { if (mFindingsHaveSameMessage) { if (mFindingMessage == null) { mFindingsHaveSameMessage = (finding.getMessage() == null); } else { mFindingsHaveSameMessage = mFindingMessage.equals(finding.getMessage()); } } getOccurrence(summary).addFinding(finding); } /** {@inheritDoc} */ public String toString () { return "[" + mFindingType + "(" + mSeverity + (mFindingsHaveSameMessage ? " " + mFindingMessage : "") + "): " + mOccurrences + "(" + mCounter + ")]"; } /** * {@inheritDoc} * Be aware that the order (result of {@link #compareTo} can change * if new findings are added. * The order is from severe with most findings to info with * fewer findings. */ public int compareTo (FindingSummary other) { int result = -mSeverity.compareTo(other.mSeverity); if (result == 0) { result = other.mCounter - mCounter; } return result; } private void addOccurrence (FindingOccurrence occurrence) { mOccurrences.put(occurrence.getFullClassName(), occurrence); } private FindingOccurrence getOccurrence (String filename) { return mOccurrences.get(filename); } public String createFindingDetailFilename () { return "finding-" + getSeverity() + "-" + getFindingType().getSymbol() + ".html"; } public void createFindingTypeContent (Writer out) throws IOException { // TODO: Handle global findings more nice final FindingOccurrence[] allFindings = mOccurrences.values().toArray(new FindingOccurrence[0]); Arrays.sort(allFindings); out.write("<h1><a href='index.html'>View by Classes</a></h1>"); out.write("<h1><a href='findings.html'>Findings - Overview</a></h1>"); out.write("<h1 title='"); out.write(getFindingType().getSymbol()); out.write("'>"); out.write(getSeverity().toString()); out.write(" "); out.write(getFindingType().getShortText()); out.write(" ("); out.write(getOrigin().toString()); out.write(")"); out.write("</h1>\n"); if (isFindingsHaveSameMessage() && getFindingMessage() != null) { out.write("<h2>"); out.write(XmlUtil.escape(getFindingMessage())); out.write("</h2>\n"); } if (getWikiPrefix() != null) { out.write("<a href='" + getWikiPrefix() + getFindingType().getSymbol() + "'>Further info on the wiki.</a>\n"); } out.write("<blockquote>\n"); out.write(getFindingType().getDescription()); out.write("</blockquote>\n"); out.write("<table border='0' cellpadding='0' cellspacing='0' " + "width='95%' summary='Places of this finding.'>"); for (final FindingSummary.FindingOccurrence occurrence : allFindings) { out.write("<tr><td class='findingtype-counter'>"); out.write(Integer.toString(occurrence.getFindings().size())); out.write("</td><td class='findingtype-class' width='100%'>"); // out.write("<a href='"); // out.write(occurrence.getHtmlLink()); // out.write("'>"); out.write(occurrence.getFullClassName()); out.write("</td></tr>"); out.write("<tr><td class='findingtype-data' colspan='2'>"); final Iterator<Item> i = occurrence.getFindings().iterator(); while (i.hasNext()) { final Item item = i.next(); final String htmlLink = occurrence.getHtmlLink(); if (htmlLink != null) { out.write("<a href='"); out.write(occurrence.getHtmlLink()); out.write("#LINE"); out.write(Integer.toString(item.getLine())); out.write("'>"); } if (!isFindingsHaveSameMessage() && item.getMessage() != null) { out.write(XmlUtil.escape(item.getMessage())); } out.write(" ["); out.write(Integer.toString(item.getLine())); if (item.getColumn() != 0) { out.write(":"); out.write(Integer.toString(item.getColumn())); } out.write("]"); if (htmlLink != null) { out.write("</a>"); } if (i.hasNext()) { out.write(", "); } if (!isFindingsHaveSameMessage() && item.getMessage() != null) { out.write("<br />"); } } out.write("</td></tr>\n"); } out.write("</table>\n"); } /** * Checks for the wiki prefix to be used. * @return the wiki prefix to be used. */ private String getWikiPrefix () { return System.getProperty(Java2Html.WIKI_BASE_PROPERTY); } /** * A occurrence of a finding. * This class encapsulates all findings of a single type in one file. * Be aware that the order (result of {@link #compareTo} can change * if new findings are added. * * @author Andreas Mandel */ final class FindingOccurrence implements Comparable<FindingOccurrence> { private final FileSummary mFileSummary; private final List<Item> mFindingsInFile = new ArrayList<Item>(); private FindingOccurrence (FileSummary summary) { mFileSummary = summary; addOccurrence(this); } /** * @return the name of the package of this class/file */ public String getPackagename () { return mFileSummary.getPackage(); } public void addFinding (Item finding) { mFindingsInFile.add(finding); mCounter++; mOverallCounter++; } public List<Item> getFindings () { return Collections.unmodifiableList(mFindingsInFile); } /** * @return ClassName including package. */ public String getFullClassName () { return mFileSummary.getFullClassName(); } public String getClassName () { return mFileSummary.getClassName(); } public String getHtmlLink () { return mFileSummary.getHtmlLink(); } public int countFindingsInFile () { return mFindingsInFile.size(); } /** {@inheritDoc} */ public String toString () { return "[" + getClassName() + ": " + findingsToString() + "(" + mFindingsInFile.size() + ")]"; } public String findingsToString () { final StringBuilder sb = new StringBuilder(); sb.append('{'); final Iterator<Item> i = mFindingsInFile.iterator(); while (i.hasNext()) { final Item finding = i.next(); sb.append('@'); sb.append(finding.getLine()); sb.append(':'); sb.append(finding.getColumn()); if (i.hasNext()) { sb.append(", "); } } sb.append('}'); return sb.toString(); } /** * {@inheritDoc} * Be aware that the order (result of {@link #compareTo} can change * if new findings are added. * The order is from most findings to fewer findings. */ public int compareTo (FindingOccurrence o) { return o.mFindingsInFile.size() - this.mFindingsInFile.size(); } } } }