/* * $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.awt.Color; import java.awt.Dimension; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.Map.Entry; import net.sourceforge.chart2d.Chart2DProperties; import net.sourceforge.chart2d.Dataset; import net.sourceforge.chart2d.GraphChart2DProperties; import net.sourceforge.chart2d.GraphProperties; import net.sourceforge.chart2d.LBChart2D; import net.sourceforge.chart2d.LegendProperties; import net.sourceforge.chart2d.MultiColorsProperties; import net.sourceforge.chart2d.Object2DProperties; import org.jcoderz.commons.util.IoUtil; import org.jcoderz.commons.util.ObjectUtil; import org.jcoderz.phoenix.report.jaxb.File; import org.jcoderz.phoenix.report.jaxb.Report; /** * TODO: Extend this class to run historic tool on existing reports. * * @author Andreas Mandel */ public final class StatisticCollector { private static final int MAX_SERVICE_PACKAGES = 15; private static final int ROW_ERROR = 0; private static final int ROW_CPD = 1; private static final int ROW_WARNING = 2; private static final int ROW_DESIGN = 3; private static final int ROW_CODE_STYLE = 4; private static final int ROW_INFO = 5; private static final int ROW_COVERAGE = 6; private static final int PERCENT = 100; private static final int LARGE_IMAGE_WIDTH = 1000; private static final int LARGE_IMAGE_HEIGHT = 600; private static final int SMALL_IMAGE_WIDTH = 800; private static final int SMALL_IMAGE_HEIGHT = 300; private static final Dimension LARGE_SIZE = new Dimension(LARGE_IMAGE_WIDTH, LARGE_IMAGE_HEIGHT); private static final Dimension SMALL_SIZE = new Dimension(SMALL_IMAGE_WIDTH, SMALL_IMAGE_HEIGHT); /** The report to be used as input. */ private final Report mReport; /** The package prefix to check for. */ private final String mPrefix; /** The dir to be used as output. */ private final java.io.File mOutDir; /** The timestamp when the report was initiated. */ private final String mTimestamp; /** * Creates a new StatisticCollector object. * * @param report the input report. * @param outDir the output dir for the charts and summary xml. * @param timestamp the timestamp for the summary db entry. */ public StatisticCollector (Report report, java.io.File outDir, String timestamp) { mReport = report; // TODO: Try to find this automagically, 2 - 3 packagelevels mPrefix = "org.jcoderz."; mOutDir = outDir; mTimestamp = timestamp; } /** * Creates a new StatisticCollector object. * * @param report the input report. * @param prefix the package prefix * @param outDir the output dir for the charts and summary xml. * @param timestamp the timestamp for the summary db entry. */ public StatisticCollector (Report report, String prefix, java.io.File outDir, String timestamp) { mReport = report; mPrefix = prefix; mOutDir = outDir; mTimestamp = timestamp; } /** * Creates the charts. * @throws IOException if the image can not be written. */ public void createCharts () throws IOException { // chart should contain x axis with packages, // y axis with number of lines // (2 lines 1 for production code one for test code) final Map<String, FileSummary> productionPackages = new HashMap<String, FileSummary>(); final Map<String, FileSummary> testPackages = new HashMap<String, FileSummary>(); final Map<String, FileSummary> allPackages = new HashMap<String, FileSummary>(); summarize(productionPackages, testPackages, allPackages); writeSummary(allPackages); allPackages.clear(); // make GC happy final Map<String, FileSummary> production; final Map<String, FileSummary> test; final String level; if (productionPackages.size() > MAX_SERVICE_PACKAGES) { production = generateServiceLevelMap(productionPackages); test = generateServiceLevelMap(testPackages); level = "Service"; } else { production = productionPackages; test = testPackages; level = "Package"; } createLocChart(production, test, level); createQualityChart(production, level); } /** * Writes a summary xml for the given summary map. * @param packages a map with the package names / summaries to be * used. * @throws IOException if the XML file can not be written */ private void writeSummary (Map<String, FileSummary> packages) throws IOException { final StringBuffer sb = new StringBuffer(); final FileSummary all = new FileSummary(); final Iterator<FileSummary> i = packages.values().iterator(); while (i.hasNext()) { all.add(i.next()); } sb.append("<findingsummary "); fillSummaryLine(sb, all); sb.append(">\n"); sb.append(" <packagelevelxml>\n"); for (Entry<String, FileSummary> entry : packages.entrySet()) { final String pkg = entry.getKey(); final FileSummary summary = entry.getValue(); sb.append(" <package name='"); sb.append(pkg); sb.append("' "); fillSummaryLine(sb, summary); sb.append("/>\n"); } sb.append(" </packagelevelxml>\n"); final Map<String, FileSummary> serviceLevelMap = generateServiceLevelMap(packages); sb.append(" <servicelevelxml>\n"); for (Entry<String, FileSummary> service : serviceLevelMap.entrySet()) { final String pkg = service.getKey(); final FileSummary summary = service.getValue(); sb.append(" <service name='"); sb.append(pkg); sb.append("' "); fillSummaryLine(sb, summary); sb.append("/>\n"); } sb.append(" </servicelevelxml>\n"); sb.append("</findingsummary>\n"); FileWriter w = null; try { final java.io.File out = new java.io.File(mOutDir, "summary.xml"); w = new FileWriter(out); w.write(sb.toString()); } finally { IoUtil.close(w); } } private Map<String, FileSummary> generateServiceLevelMap ( Map<String, FileSummary> packages) { final Map<String, FileSummary> serviceLevelMap = new HashMap<String, FileSummary>(); for (Entry<String, FileSummary> packagz : packages.entrySet()) { final String pkg = packagz.getKey(); final FileSummary summary = packagz.getValue(); final String service = getService(pkg); FileSummary serviceSummary = serviceLevelMap.get(service); if (serviceSummary == null) { serviceSummary = new FileSummary(service); serviceLevelMap.put(service, serviceSummary); } serviceSummary.add(summary); } return serviceLevelMap; } private void fillSummaryLine (final StringBuffer sb, FileSummary summary) { sb.append("timestamp='"); sb.append(mTimestamp); sb.append("' "); for (int i = 0; i < Severity.VALUES.size(); i++) { final Severity currentSeverity = Severity.fromInt(i); sb.append(currentSeverity.toString()); sb.append("='"); sb.append(summary.getViolations(currentSeverity)); sb.append("' "); } sb.append(" loc='"); sb.append(summary.getLinesOfCode()); sb.append("' codeLoc='"); sb.append(summary.getCoverage() + summary.getViolations(Severity.COVERAGE)); sb.append("' quality='"); sb.append(summary.getQualityAsFloat()); sb.append('\''); } private String getService (String pkg) { String result; if (pkg != null && pkg.startsWith(mPrefix)) { result = pkg.substring(mPrefix.length()); if (result.indexOf('.') != -1) { result = result.substring(result.indexOf('.') + 1); } if (result.indexOf('.') != -1) { result = result.substring(0, result.indexOf('.')); } } else { result = ObjectUtil.toStringOrEmpty(pkg); } return result; } /** * Collects summary data and puts the results in the given maps. * @param productionPackages map to collect production code data. * @param testPackages map to collect test code data. * @param all All packages. */ private void summarize (final Map<String, FileSummary> productionPackages, final Map<String, FileSummary> testPackages, final Map<String, FileSummary> all) { final List<File> allFiles = mReport.getFile(); for (final File currentFile : allFiles) { // Level 3 is currently given to test classes, below 3 is production if (currentFile.getLevel().equals(ReportLevel.TEST)) { addToMap(testPackages, currentFile); addToMap(all, currentFile); } else { addToMap(productionPackages, currentFile); addToMap(all, currentFile); } } } private FileSummary addToMap (final Map<String, FileSummary> map, final File file) { final String pkg = file.getPackage(); FileSummary counter = map.get(pkg); if (counter == null) { counter = new FileSummary(pkg); map.put(pkg, counter); } calculateSummary(file, counter); return counter; } /** * Collects finding summary for a concrete file. * @param currentFile the file structure. * @param counter the counter structure. */ private void calculateSummary (File currentFile, FileSummary counter) { counter.add(currentFile); } /** * Creates the lines of code chart. * Locs are painted per package. Separated by test and production code. * The output is a "loc.png". * * @throws IOException if the image can not be written. */ private void createLocChart (Map<String, FileSummary> src, Map<String, FileSummary> test, String level) throws IOException { //<-- Begin Chart2D configuration --> //Configure object properties final Object2DProperties object2DProps = new Object2DProperties(); object2DProps.setObjectTitleText ("LOC by " + level); //Configure chart properties final Chart2DProperties chart2DProps = new Chart2DProperties(); chart2DProps.setChartDataLabelsPrecision (1); //Configure legend properties final LegendProperties legendProps = new LegendProperties(); final String[] legendLabels = {"Production", "Test"}; legendProps.setLegendLabelsTexts (legendLabels); //Configure graph chart properties final GraphChart2DProperties graphChart2DProps = new GraphChart2DProperties(); final Set<String> labels = new TreeSet<String>(src.keySet()); labels.addAll(test.keySet()); if (labels.size() == 0) { throw new RuntimeException("No packages found for chart!"); } final String [] labelsLongAxisLabels = labels.toArray(new String[]{}); final String [] labelsAxisLabels = cutPackages(labelsLongAxisLabels); graphChart2DProps.setLabelsAxisLabelsTexts(labelsAxisLabels); graphChart2DProps.setLabelsAxisTitleText(level + " Name"); graphChart2DProps.setNumbersAxisTitleText("LOC"); graphChart2DProps.setLabelsAxisTicksAlignment( GraphChart2DProperties.CENTERED); //Configure graph properties final GraphProperties graphProps = new GraphProperties(); graphProps.setGraphBarsExistence(false); graphProps.setGraphDotsExistence(true); graphProps.setGraphAllowComponentAlignment(true); graphProps.setGraphDotsWithinCategoryOverlapRatio(1); //Configure dataset final Dataset dataset = new Dataset (legendLabels.length, labelsAxisLabels.length, 1); // fill data.... for (int j = 0; j < dataset.getNumCats(); ++j) { dataset.set(0, j, 0, getCounter(src, labelsLongAxisLabels[j])); dataset.set(1, j, 0, getCounter(test, labelsLongAxisLabels[j])); } //Configure graph component colors final MultiColorsProperties multiColorsProps = new MultiColorsProperties(); //Configure chart final LBChart2D chart2D = new LBChart2D(); chart2D.setObject2DProperties (object2DProps); chart2D.setChart2DProperties (chart2DProps); chart2D.setLegendProperties (legendProps); chart2D.setGraphChart2DProperties (graphChart2DProps); chart2D.addGraphProperties (graphProps); chart2D.addDataset (dataset); chart2D.addMultiColorsProperties (multiColorsProps); //<-- End Chart2D configuration --> chart2D.setMaximumSize(LARGE_SIZE); chart2D.setPreferredSize(LARGE_SIZE); if (chart2D.validate(false)) { java.io.File file = new java.io.File(mOutDir, "loc_large.png"); javax.imageio.ImageIO.write(chart2D.getImage(), "PNG", file); chart2D.setMaximumSize(SMALL_SIZE); chart2D.setPreferredSize(SMALL_SIZE); chart2D.pack(); file = new java.io.File(mOutDir, "loc_small.png"); javax.imageio.ImageIO.write(chart2D.getImage(), "PNG", file); } else { chart2D.validate(true); } } /** * Creates the quality chart. * The output is a "quality.png". * * @throws IOException if the image can not be written. */ private void createQualityChart (Map<String, FileSummary> src, String level) throws IOException { //<-- Begin Chart2D configuration --> //Configure object properties final Object2DProperties object2DProps = new Object2DProperties(); object2DProps.setObjectTitleText ("Quality by " + level); //Configure chart properties final Chart2DProperties chart2DProps = new Chart2DProperties(); chart2DProps.setChartDataLabelsPrecision(0); //Configure legend properties final LegendProperties legendProps = new LegendProperties(); final String[] legendLabels = { "Error", "C&P", "Warning", "Design", "Code Style", "Info", "Coverage"}; legendProps.setLegendLabelsTexts (legendLabels); //Configure graph chart properties final GraphChart2DProperties graphChart2DProps = new GraphChart2DProperties(); final Set<String> labels = new TreeSet<String>(src.keySet()); if (labels.size() == 0) { throw new RuntimeException("No packages found for chart!"); } final String [] labelsLongAxisLabels = labels.toArray(new String[labels.size()]); final String [] labelsAxisLabels = cutPackages(labelsLongAxisLabels); graphChart2DProps.setLabelsAxisLabelsTexts(labelsAxisLabels); graphChart2DProps.setLabelsAxisTitleText(level + " Name"); graphChart2DProps.setNumbersAxisTitleText("Finding/Loc %"); graphChart2DProps.setLabelsAxisTicksAlignment( GraphChart2DProperties.CENTERED); //Configure graph properties final GraphProperties graphProps = new GraphProperties(); graphProps.setGraphBarsExistence(false); graphProps.setGraphDotsExistence(true); graphProps.setGraphAllowComponentAlignment(true); graphProps.setGraphDotsWithinCategoryOverlapRatio(1); //Configure dataset final Dataset dataset = new Dataset (legendLabels.length, labelsAxisLabels.length, 1); // fill data.... for (int j = 0; j < dataset.getNumCats(); ++j) { final FileSummary sum = src.get(labelsLongAxisLabels[j]); if (sum != null && sum.getLinesOfCode() != 0) { final float loc = sum.getLinesOfCode(); final float codeLoc = sum.getCoverage() + sum.getViolations(Severity.COVERAGE); dataset.set(ROW_ERROR, j, 0, (PERCENT * sum.getViolations(Severity.ERROR)) / loc); dataset.set(ROW_CPD, j, 0, (PERCENT * sum.getViolations(Severity.CPD)) / loc); dataset.set(ROW_WARNING, j, 0, (PERCENT * sum.getViolations(Severity.WARNING)) / loc); dataset.set(ROW_DESIGN, j, 0, (PERCENT * sum.getViolations(Severity.DESIGN)) / loc); dataset.set(ROW_CODE_STYLE, j, 0, (PERCENT * sum.getViolations(Severity.CODE_STYLE)) / loc); dataset.set(ROW_INFO, j, 0, (PERCENT * sum.getViolations(Severity.INFO)) / loc); if (codeLoc != 0) { dataset.set(ROW_COVERAGE, j, 0, (PERCENT * sum.getCoverage()) / codeLoc); } else { dataset.set(ROW_COVERAGE, j, 0, PERCENT); } } else { dataset.set(ROW_ERROR, j, 0, 0); dataset.set(ROW_CPD, j, 0, 0); dataset.set(ROW_WARNING, j, 0, 0); dataset.set(ROW_DESIGN, j, 0, 0); dataset.set(ROW_CODE_STYLE, j, 0, 0); dataset.set(ROW_INFO, j, 0, 0); dataset.set(ROW_COVERAGE, j, 0, 0); } } //Configure graph component colors final MultiColorsProperties multiColorsProps = new MultiColorsProperties(); multiColorsProps.setColorsCustomize(true); multiColorsProps.setColorsCustom(new Color[] { Color.RED, Color.BLUE, Color.ORANGE, Color.YELLOW, Color.YELLOW, Color.CYAN, Color.MAGENTA }); //Configure chart final LBChart2D chart2D = new LBChart2D(); chart2D.setObject2DProperties (object2DProps); chart2D.setChart2DProperties (chart2DProps); chart2D.setLegendProperties (legendProps); chart2D.setGraphChart2DProperties (graphChart2DProps); chart2D.addGraphProperties (graphProps); chart2D.addDataset (dataset); chart2D.addMultiColorsProperties (multiColorsProps); //<-- End Chart2D configuration --> chart2D.setMaximumSize(LARGE_SIZE); chart2D.setPreferredSize(LARGE_SIZE); if (chart2D.validate(false)) { java.io.File file = new java.io.File(mOutDir, "quality_large.png"); javax.imageio.ImageIO.write(chart2D.getImage(), "PNG", file); chart2D.setMaximumSize(SMALL_SIZE); chart2D.setPreferredSize(SMALL_SIZE); chart2D.pack(); file = new java.io.File(mOutDir, "quality_small.png"); javax.imageio.ImageIO.write(chart2D.getImage(), "PNG", file); } else { chart2D.validate(true); } } /** * Gets the loc counter for a given package. */ private float getCounter (Map<String, FileSummary> src, String string) { final FileSummary counter = src.get(string); final int result; if (counter != null) { result = counter.getLinesOfCode(); } else { result = 0; } return result; } /** * Returns an array with the short for of the given package name. * @param labelsAxisLabels an array with package names to be cut. * @return a string array containing short form of the input package names. */ private String[] cutPackages (String[] labelsAxisLabels) { final String [] result = new String[labelsAxisLabels.length]; for (int i = 0; i < labelsAxisLabels.length; i++) { if (labelsAxisLabels[i].startsWith(mPrefix)) { result[i] = labelsAxisLabels[i].substring(mPrefix.length()); if (result[i].indexOf('.') != -1) { result[i] = result[i].substring(result[i].indexOf('.') + 1); } } else { result[i] = labelsAxisLabels[i]; } } return result; } }