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    }