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}