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 */
025 package net.sourceforge.cobertura.reporting;
026
027 import java.io.IOException;
028 import java.util.HashMap;
029 import java.util.Iterator;
030 import java.util.List;
031 import java.util.Map;
032
033 import net.sourceforge.cobertura.coveragedata.ClassData;
034 import net.sourceforge.cobertura.coveragedata.PackageData;
035 import net.sourceforge.cobertura.coveragedata.ProjectData;
036 import net.sourceforge.cobertura.coveragedata.SourceFileData;
037 import net.sourceforge.cobertura.javancss.FunctionMetric;
038 import net.sourceforge.cobertura.javancss.Javancss;
039 import net.sourceforge.cobertura.util.FileFinder;
040 import net.sourceforge.cobertura.util.Source;
041
042 import 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 */
055 public 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 }