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.File;
028 import java.io.InputStream;
029 import java.io.IOException;
030 import java.util.Enumeration;
031 import java.util.HashMap;
032 import java.util.Iterator;
033 import java.util.Map;
034 import java.util.Vector;
035
036 import net.sourceforge.cobertura.coveragedata.ClassData;
037 import net.sourceforge.cobertura.coveragedata.PackageData;
038 import net.sourceforge.cobertura.coveragedata.ProjectData;
039 import net.sourceforge.cobertura.coveragedata.SourceFileData;
040 import net.sourceforge.cobertura.javancss.Javancss;
041 import net.sourceforge.cobertura.javancss.JavancssConstants;
042 import net.sourceforge.cobertura.util.FileFinder;
043 import net.sourceforge.cobertura.util.Source;
044
045 import org.apache.log4j.Logger;
046
047
048 /**
049 * Allows complexity computing for source files, packages and a whole project. Average
050 * McCabe's number for methods contained in the specified entity is returned. This class
051 * depends on FileFinder which is used to map source file names to existing files.
052 *
053 * <p>One instance of this class should be used for the same set of source files - an
054 * object of this class can cache computed results.</p>
055 *
056 * @author Grzegorz Lukasik
057 */
058 public class ComplexityCalculator {
059 private static final Logger logger = Logger.getLogger(ComplexityCalculator.class);
060
061 public static final Complexity ZERO_COMPLEXITY = new Complexity();
062
063 // Finder used to map source file names to existing files
064 private final FileFinder finder;
065
066 // Contains pairs (String sourceFileName, Complexity complexity)
067 private Map sourceFileCNNCache = new HashMap();
068
069 // Contains pairs (String packageName, Complexity complexity)
070 private Map packageCNNCache = new HashMap();
071
072 /**
073 * Creates new calculator. Passed {@link FileFinder} will be used to
074 * map source file names to existing files when needed.
075 *
076 * @param finder {@link FileFinder} that allows to find source files
077 * @throws NullPointerException if finder is null
078 */
079 public ComplexityCalculator( FileFinder finder) {
080 if( finder==null)
081 throw new NullPointerException();
082 this.finder = finder;
083 }
084
085 /**
086 * Calculates the code complexity number for an input stream.
087 * "CCN" stands for "code complexity number." This is
088 * sometimes referred to as McCabe's number. This method
089 * calculates the average cyclomatic code complexity of all
090 * methods of all classes in a given directory.
091 *
092 * @param file The input stream for which you want to calculate
093 * the complexity
094 * @return average complexity for the specified input stream
095 */
096 private Complexity getAccumlatedCCNForSource(String sourceFileName, Source source) {
097 if (source == null)
098 {
099 return ZERO_COMPLEXITY;
100 }
101 Javancss javancss = new Javancss(source.getInputStream());
102
103 if (javancss.getLastErrorMessage() != null)
104 {
105 //there is an error while parsing the java file. log it
106 logger.warn("JavaNCSS got an error while parsing the java " + source.getOriginDesc() + "\n"
107 + javancss.getLastErrorMessage());
108 }
109
110 Vector methodMetrics = javancss.getFunctionMetrics();
111 int classCcn = 0;
112 for( Enumeration method = methodMetrics.elements(); method.hasMoreElements();)
113 {
114 Vector singleMethodMetrics = (Vector)method.nextElement();
115 classCcn += ((Integer)singleMethodMetrics.elementAt(JavancssConstants.FCT_CCN)).intValue();
116 }
117
118 return new Complexity( classCcn, methodMetrics.size());
119 }
120
121 /**
122 * Calculates the code complexity number for single source file.
123 * "CCN" stands for "code complexity number." This is
124 * sometimes referred to as McCabe's number. This method
125 * calculates the average cyclomatic code complexity of all
126 * methods of all classes in a given directory.
127 * @param sourceFileName
128 *
129 * @param file The source file for which you want to calculate
130 * the complexity
131 * @return average complexity for the specified source file
132 * @throws IOException
133 */
134 private Complexity getAccumlatedCCNForSingleFile(String sourceFileName) throws IOException {
135 Source source = finder.getSource(sourceFileName);
136 try
137 {
138 return getAccumlatedCCNForSource(sourceFileName, source);
139 }
140 finally
141 {
142 if (source != null)
143 {
144 source.close();
145 }
146 }
147 }
148
149 /**
150 * Computes CCN for all sources contained in the project.
151 * CCN for whole project is an average CCN for source files.
152 * All source files for which CCN cannot be computed are ignored.
153 *
154 * @param projectData project to compute CCN for
155 * @throws NullPointerException if projectData is null
156 * @return CCN for project or 0 if no source files were found
157 */
158 public double getCCNForProject( ProjectData projectData) {
159 // Sum complexity for all packages
160 Complexity act = new Complexity();
161 for( Iterator it = projectData.getPackages().iterator(); it.hasNext();) {
162 PackageData packageData = (PackageData)it.next();
163 act.add( getCCNForPackageInternal( packageData));
164 }
165
166 // Return average CCN for source files
167 return act.averageCCN();
168 }
169
170 /**
171 * Computes CCN for all sources contained in the specified package.
172 * All source files that cannot be mapped to existing files are ignored.
173 *
174 * @param packageData package to compute CCN for
175 * @throws NullPointerException if <code>packageData</code> is <code>null</code>
176 * @return CCN for the specified package or 0 if no source files were found
177 */
178 public double getCCNForPackage(PackageData packageData) {
179 return getCCNForPackageInternal(packageData).averageCCN();
180 }
181
182 private Complexity getCCNForPackageInternal(PackageData packageData) {
183 // Return CCN if computed earlier
184 Complexity cachedCCN = (Complexity) packageCNNCache.get( packageData.getName());
185 if( cachedCCN!=null) {
186 return cachedCCN;
187 }
188
189 // Compute CCN for all source files inside package
190 Complexity act = new Complexity();
191 for( Iterator it = packageData.getSourceFiles().iterator(); it.hasNext();) {
192 SourceFileData sourceData = (SourceFileData)it.next();
193 act.add( getCCNForSourceFileNameInternal( sourceData.getName()));
194 }
195
196 // Cache result and return it
197 packageCNNCache.put( packageData.getName(), act);
198 return act;
199 }
200
201
202 /**
203 * Computes CCN for single source file.
204 *
205 * @param sourceFile source file to compute CCN for
206 * @throws NullPointerException if <code>sourceFile</code> is <code>null</code>
207 * @return CCN for the specified source file, 0 if cannot map <code>sourceFile</code> to existing file
208 */
209 public double getCCNForSourceFile(SourceFileData sourceFile) {
210 return getCCNForSourceFileNameInternal( sourceFile.getName()).averageCCN();
211 }
212
213 private Complexity getCCNForSourceFileNameInternal(String sourceFileName) {
214 // Return CCN if computed earlier
215 Complexity cachedCCN = (Complexity) sourceFileCNNCache.get( sourceFileName);
216 if( cachedCCN!=null) {
217 return cachedCCN;
218 }
219
220 // Compute CCN and cache it for further use
221 Complexity result = ZERO_COMPLEXITY;
222 try {
223 result = getAccumlatedCCNForSingleFile( sourceFileName );
224 } catch( IOException ex) {
225 logger.info( "Cannot find source file during CCN computation, source=["+sourceFileName+"]");
226 }
227 sourceFileCNNCache.put( sourceFileName, result);
228 return result;
229 }
230
231 /**
232 * Computes CCN for source file the specified class belongs to.
233 *
234 * @param classData package to compute CCN for
235 * @return CCN for source file the specified class belongs to
236 * @throws NullPointerException if <code>classData</code> is <code>null</code>
237 */
238 public double getCCNForClass(ClassData classData) {
239 return getCCNForSourceFileNameInternal( classData.getSourceFileName()).averageCCN();
240 }
241
242
243 /**
244 * Represents complexity of source file, package or project. Stores the number of
245 * methods inside entity and accumlated complexity for these methods.
246 */
247 private static class Complexity {
248 private double accumlatedCCN;
249 private int methodsNum;
250 public Complexity(double accumlatedCCN, int methodsNum) {
251 this.accumlatedCCN = accumlatedCCN;
252 this.methodsNum = methodsNum;
253 }
254 public Complexity() {
255 this(0,0);
256 }
257 public double averageCCN() {
258 if( methodsNum==0) {
259 return 0;
260 }
261 return accumlatedCCN/methodsNum;
262 }
263 public void add( Complexity second) {
264 accumlatedCCN += second.accumlatedCCN;
265 methodsNum += second.methodsNum;
266 }
267 }
268 }