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     *
008     * Cobertura is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License as published
010     * by the Free Software Foundation; either version 2 of the License,
011     * or (at your option) any later version.
012     *
013     * Cobertura is distributed in the hope that it will be useful, but
014     * WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016     * General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with Cobertura; if not, write to the Free Software
020     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
021     * USA
022     */
023    package net.sourceforge.cobertura.reporting;
024    
025    import java.io.File;
026    import java.io.IOException;
027    import java.util.HashMap;
028    import java.util.Iterator;
029    import java.util.List;
030    import java.util.Map;
031    
032    import net.sourceforge.cobertura.coveragedata.ClassData;
033    import net.sourceforge.cobertura.coveragedata.PackageData;
034    import net.sourceforge.cobertura.coveragedata.ProjectData;
035    import net.sourceforge.cobertura.coveragedata.SourceFileData;
036    import net.sourceforge.cobertura.javancss.Javancss;
037    import net.sourceforge.cobertura.util.FileFinder;
038    
039    import org.apache.log4j.Logger;
040    
041    
042    /**
043     * Allows complexity computing for source files, packages and a whole project. Average
044     * McCabe's number for methods contained in the specified entity is returned. This class
045     * depends on FileFinder which is used to map source file names to existing files.
046     * 
047     * <p>One instance of this class should be used for the same set of source files - an 
048     * object of this class can cache computed results.</p>
049     * 
050     * @author Grzegorz Lukasik
051     */
052    public class ComplexityCalculator {
053            private static final Logger logger = Logger.getLogger(ComplexityCalculator.class);
054    
055            public static final Complexity ZERO_COMPLEXITY = new Complexity(0,0);
056            
057            // Finder used to map source file names to existing files
058            private final FileFinder finder;
059            
060            // Contains pairs (String sourceFileName, Complexity complexity)
061            private Map sourceFileCNNCache = new HashMap();
062    
063            // Contains pairs (String packageName, Complexity complexity)
064            private Map packageCNNCache = new HashMap();
065    
066            /**
067             * Creates new calculator. Passed {@link FileFinder} will be used to 
068             * map source file names to existing files when needed. 
069             * 
070             * @param finder {@link FileFinder} that allows to find source files
071             * @throws NullPointerException if finder is null
072             */
073            public ComplexityCalculator( FileFinder finder) {
074                    if( finder==null)
075                            throw new NullPointerException();
076                    this.finder = finder;
077            }
078            
079            /**
080             * Calculates the code complexity number for single source file.
081             * "CCN" stands for "code complexity number."  This is
082             * sometimes referred to as McCabe's number.  This method
083             * calculates the average cyclomatic code complexity of all
084             * methods of all classes in a given directory.  
085             *
086             * @param file The source file for which you want to calculate
087             *        the complexity
088             * @return average complexity for the specified source file 
089             */
090            private Complexity getAccumlatedCCNForSingleFile(File file) {
091                    Javancss javancss = new Javancss(file.getAbsolutePath());
092    
093                    List methodComplexities = javancss.getMethodComplexities();
094                    if (methodComplexities.size() <= 0)
095                            return ZERO_COMPLEXITY;
096    
097                    int ccnAccumulator = 0;
098                    Iterator iter = methodComplexities.iterator();
099                    while (iter.hasNext())
100                    {
101                            ccnAccumulator += ((Integer)iter.next()).intValue();
102                    }
103                    
104                    return new Complexity( ccnAccumulator, methodComplexities.size());
105            }
106    
107    
108            /**
109             * Computes CCN for all sources contained in the project.
110             * CCN for whole project is an average CCN for source files.
111             * All source files for which CCN cannot be computed are ignored.
112             * 
113             * @param projectData project to compute CCN for
114             * @throws NullPointerException if projectData is null
115             * @return CCN for project or 0 if no source files were found
116             */
117            public double getCCNForProject( ProjectData projectData) {
118                    // Sum complexity for all packages
119                    Complexity act = new Complexity();
120                    for( Iterator it = projectData.getPackages().iterator(); it.hasNext();) {
121                            PackageData packageData = (PackageData)it.next();
122                            act.add( getCCNForPackageInternal( packageData));
123                    }
124    
125                    // Return average CCN for source files
126                    return act.averageCCN();
127            }
128            
129            /**
130             * Computes CCN for all sources contained in the specified package.
131             * All source files that cannot be mapped to existing files are ignored.
132             * 
133             * @param packageData package to compute CCN for
134             * @throws NullPointerException if <code>packageData</code> is <code>null</code>
135             * @return CCN for the specified package or 0 if no source files were found
136             */
137            public double getCCNForPackage(PackageData packageData) {
138                    return getCCNForPackageInternal(packageData).averageCCN();
139            }
140    
141            private Complexity getCCNForPackageInternal(PackageData packageData) {
142                    // Return CCN if computed earlier
143                    Complexity cachedCCN = (Complexity) packageCNNCache.get( packageData.getName());
144                    if( cachedCCN!=null) {
145                            return cachedCCN;
146                    }
147                    
148                    // Compute CCN for all source files inside package
149                    Complexity act = new Complexity();
150                    for( Iterator it = packageData.getSourceFiles().iterator(); it.hasNext();) {
151                            SourceFileData sourceData = (SourceFileData)it.next();
152                            act.add( getCCNForSourceFileNameInternal( sourceData.getName()));
153                    }
154                    
155                    // Cache result and return it
156                    packageCNNCache.put( packageData.getName(), act);
157                    return act;
158            }
159    
160            
161            /**
162             * Computes CCN for single source file.
163             * 
164             * @param sourceFile source file to compute CCN for
165             * @throws NullPointerException if <code>sourceFile</code> is <code>null</code>
166             * @return CCN for the specified source file, 0 if cannot map <code>sourceFile</code> to existing file
167             */
168            public double getCCNForSourceFile(SourceFileData sourceFile) {
169                    return getCCNForSourceFileNameInternal( sourceFile.getName()).averageCCN();
170            }
171    
172            private Complexity getCCNForSourceFileNameInternal(String sourceFileName) {
173                    // Return CCN if computed earlier
174                    Complexity cachedCCN = (Complexity) sourceFileCNNCache.get( sourceFileName);
175                    if( cachedCCN!=null) {
176                            return cachedCCN;
177                    }
178    
179                // Compute CCN and cache it for further use
180                    Complexity result = ZERO_COMPLEXITY;
181                    try {
182                            result = getAccumlatedCCNForSingleFile( finder.getFileForSource(sourceFileName));
183                    } catch( IOException ex) {
184                            logger.info( "Cannot find source file during CCN computation, source=["+sourceFileName+"]");
185                    }
186                    sourceFileCNNCache.put( sourceFileName, result);
187                    return result;
188            }
189    
190            /**
191             * Computes CCN for source file the specified class belongs to.
192             * 
193             * @param classData package to compute CCN for
194             * @return CCN for source file the specified class belongs to
195             * @throws NullPointerException if <code>classData</code> is <code>null</code>
196             */
197            public double getCCNForClass(ClassData classData) {
198                    return getCCNForSourceFileNameInternal( classData.getSourceFileName()).averageCCN();
199            }
200    
201    
202            /**
203             * Represents complexity of source file, package or project. Stores the number of
204             * methods inside entity and accumlated complexity for these methods.
205             */
206            private static class Complexity {
207                    private double accumlatedCCN;
208                    private int methodsNum;
209                    public Complexity(double accumlatedCCN, int methodsNum) {
210                            this.accumlatedCCN = accumlatedCCN;
211                            this.methodsNum = methodsNum;
212                    }
213                    public Complexity() {
214                            this(0,0);
215                    }
216                    public double averageCCN() {
217                            if( methodsNum==0) {
218                                    return 0;
219                            }
220                            return accumlatedCCN/methodsNum;
221                    }
222                    public void add( Complexity second) {
223                            accumlatedCCN += second.accumlatedCCN;
224                            methodsNum += second.methodsNum;
225                    }
226            }
227    }