001/* 002 * Cobertura - http://cobertura.sourceforge.net/ 003 * 004 * Copyright (C) 2005 Mark Doliner 005 * Copyright (C) 2005 Jeremy Thomerson 006 * Copyright (C) 2005 Grzegorz Lukasik 007 * Copyright (C) 2008 Tri Bao Ho 008 * Copyright (C) 2009 John Lewis 009 * 010 * Cobertura is free software; you can redistribute it and/or modify 011 * it under the terms of the GNU General Public License as published 012 * by the Free Software Foundation; either version 2 of the License, 013 * or (at your option) any later version. 014 * 015 * Cobertura is distributed in the hope that it will be useful, but 016 * WITHOUT ANY WARRANTY; without even the implied warranty of 017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 018 * General Public License for more details. 019 * 020 * You should have received a copy of the GNU General Public License 021 * along with Cobertura; if not, write to the Free Software 022 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 023 * USA 024 */ 025package net.sourceforge.cobertura.reporting; 026 027import java.io.IOException; 028import java.util.HashMap; 029import java.util.Iterator; 030import java.util.List; 031import java.util.Map; 032 033import net.sourceforge.cobertura.coveragedata.ClassData; 034import net.sourceforge.cobertura.coveragedata.PackageData; 035import net.sourceforge.cobertura.coveragedata.ProjectData; 036import net.sourceforge.cobertura.coveragedata.SourceFileData; 037import net.sourceforge.cobertura.javancss.FunctionMetric; 038import net.sourceforge.cobertura.javancss.Javancss; 039import net.sourceforge.cobertura.util.FileFinder; 040import net.sourceforge.cobertura.util.Source; 041 042import org.apache.log4j.Logger; 043 044 045/** 046 * Allows complexity computing for source files, packages and a whole project. Average 047 * McCabe's number for methods contained in the specified entity is returned. This class 048 * depends on FileFinder which is used to map source file names to existing files. 049 * 050 * <p>One instance of this class should be used for the same set of source files - an 051 * object of this class can cache computed results.</p> 052 * 053 * @author Grzegorz Lukasik 054 */ 055public class ComplexityCalculator { 056 private static final Logger logger = Logger.getLogger(ComplexityCalculator.class); 057 058 public static final Complexity ZERO_COMPLEXITY = new Complexity(); 059 060 // Finder used to map source file names to existing files 061 private final FileFinder finder; 062 063 // Contains pairs (String sourceFileName, Complexity complexity) 064 private Map sourceFileCNNCache = new HashMap(); 065 066 // Contains pairs (String packageName, Complexity complexity) 067 private Map packageCNNCache = new HashMap(); 068 069 /** 070 * Creates new calculator. Passed {@link FileFinder} will be used to 071 * map source file names to existing files when needed. 072 * 073 * @param finder {@link FileFinder} that allows to find source files 074 * @throws NullPointerException if finder is null 075 */ 076 public ComplexityCalculator( FileFinder finder) { 077 if( finder==null) 078 throw new NullPointerException(); 079 this.finder = finder; 080 } 081 082 /** 083 * Calculates the code complexity number for an input stream. 084 * "CCN" stands for "code complexity number." This is 085 * sometimes referred to as McCabe's number. This method 086 * calculates the average cyclomatic code complexity of all 087 * methods of all classes in a given directory. 088 * 089 * @param file The input stream for which you want to calculate 090 * the complexity 091 * @return average complexity for the specified input stream 092 */ 093 private Complexity getAccumlatedCCNForSource(String sourceFileName, Source source) { 094 if (source == null) 095 { 096 return ZERO_COMPLEXITY; 097 } 098 if (!sourceFileName.endsWith(".java")) 099 { 100 return ZERO_COMPLEXITY; 101 } 102 Javancss javancss = new Javancss(source.getInputStream()); 103 104 if (javancss.getLastErrorMessage() != null) 105 { 106 //there is an error while parsing the java file. log it 107 logger.warn("JavaNCSS got an error while parsing the java " + source.getOriginDesc() + "\n" 108 + javancss.getLastErrorMessage()); 109 } 110 111 List methodMetrics = javancss.getFunctionMetrics(); 112 int classCcn = 0; 113 for( Iterator method = methodMetrics.iterator(); method.hasNext();) 114 { 115 FunctionMetric singleMethodMetrics = (FunctionMetric)method.next(); 116 classCcn += singleMethodMetrics.ccn; 117 } 118 119 return new Complexity( classCcn, methodMetrics.size()); 120 } 121 122 /** 123 * Calculates the code complexity number for single source file. 124 * "CCN" stands for "code complexity number." This is 125 * sometimes referred to as McCabe's number. This method 126 * calculates the average cyclomatic code complexity of all 127 * methods of all classes in a given directory. 128 * @param sourceFileName 129 * 130 * @param file The source file for which you want to calculate 131 * the complexity 132 * @return average complexity for the specified source file 133 * @throws IOException 134 */ 135 private Complexity getAccumlatedCCNForSingleFile(String sourceFileName) throws IOException { 136 Source source = finder.getSource(sourceFileName); 137 try 138 { 139 return getAccumlatedCCNForSource(sourceFileName, source); 140 } 141 finally 142 { 143 if (source != null) 144 { 145 source.close(); 146 } 147 } 148 } 149 150 /** 151 * Computes CCN for all sources contained in the project. 152 * CCN for whole project is an average CCN for source files. 153 * All source files for which CCN cannot be computed are ignored. 154 * 155 * @param projectData project to compute CCN for 156 * @throws NullPointerException if projectData is null 157 * @return CCN for project or 0 if no source files were found 158 */ 159 public double getCCNForProject( ProjectData projectData) { 160 // Sum complexity for all packages 161 Complexity act = new Complexity(); 162 for( Iterator it = projectData.getPackages().iterator(); it.hasNext();) { 163 PackageData packageData = (PackageData)it.next(); 164 act.add( getCCNForPackageInternal( packageData)); 165 } 166 167 // Return average CCN for source files 168 return act.averageCCN(); 169 } 170 171 /** 172 * Computes CCN for all sources contained in the specified package. 173 * All source files that cannot be mapped to existing files are ignored. 174 * 175 * @param packageData package to compute CCN for 176 * @throws NullPointerException if <code>packageData</code> is <code>null</code> 177 * @return CCN for the specified package or 0 if no source files were found 178 */ 179 public double getCCNForPackage(PackageData packageData) { 180 return getCCNForPackageInternal(packageData).averageCCN(); 181 } 182 183 private Complexity getCCNForPackageInternal(PackageData packageData) { 184 // Return CCN if computed earlier 185 Complexity cachedCCN = (Complexity) packageCNNCache.get( packageData.getName()); 186 if( cachedCCN!=null) { 187 return cachedCCN; 188 } 189 190 // Compute CCN for all source files inside package 191 Complexity act = new Complexity(); 192 for( Iterator it = packageData.getSourceFiles().iterator(); it.hasNext();) { 193 SourceFileData sourceData = (SourceFileData)it.next(); 194 act.add( getCCNForSourceFileNameInternal( sourceData.getName())); 195 } 196 197 // Cache result and return it 198 packageCNNCache.put( packageData.getName(), act); 199 return act; 200 } 201 202 203 /** 204 * Computes CCN for single source file. 205 * 206 * @param sourceFile source file to compute CCN for 207 * @throws NullPointerException if <code>sourceFile</code> is <code>null</code> 208 * @return CCN for the specified source file, 0 if cannot map <code>sourceFile</code> to existing file 209 */ 210 public double getCCNForSourceFile(SourceFileData sourceFile) { 211 return getCCNForSourceFileNameInternal( sourceFile.getName()).averageCCN(); 212 } 213 214 private Complexity getCCNForSourceFileNameInternal(String sourceFileName) { 215 // Return CCN if computed earlier 216 Complexity cachedCCN = (Complexity) sourceFileCNNCache.get( sourceFileName); 217 if( cachedCCN!=null) { 218 return cachedCCN; 219 } 220 221 // Compute CCN and cache it for further use 222 Complexity result = ZERO_COMPLEXITY; 223 try { 224 result = getAccumlatedCCNForSingleFile( sourceFileName ); 225 } catch( IOException ex) { 226 logger.info( "Cannot find source file during CCN computation, source=["+sourceFileName+"]"); 227 } 228 sourceFileCNNCache.put( sourceFileName, result); 229 return result; 230 } 231 232 /** 233 * Computes CCN for source file the specified class belongs to. 234 * 235 * @param classData package to compute CCN for 236 * @return CCN for source file the specified class belongs to 237 * @throws NullPointerException if <code>classData</code> is <code>null</code> 238 */ 239 public double getCCNForClass(ClassData classData) { 240 return getCCNForSourceFileNameInternal( classData.getSourceFileName()).averageCCN(); 241 } 242 243 244 /** 245 * Represents complexity of source file, package or project. Stores the number of 246 * methods inside entity and accumlated complexity for these methods. 247 */ 248 private static class Complexity { 249 private double accumlatedCCN; 250 private int methodsNum; 251 public Complexity(double accumlatedCCN, int methodsNum) { 252 this.accumlatedCCN = accumlatedCCN; 253 this.methodsNum = methodsNum; 254 } 255 public Complexity() { 256 this(0,0); 257 } 258 public double averageCCN() { 259 if( methodsNum==0) { 260 return 0; 261 } 262 return accumlatedCCN/methodsNum; 263 } 264 public void add( Complexity second) { 265 accumlatedCCN += second.accumlatedCCN; 266 methodsNum += second.methodsNum; 267 } 268 } 269}