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    }