001    /*
002     * Cobertura - http://cobertura.sourceforge.net/
003     *
004     * Copyright (C) 2003 jcoverage ltd.
005     * Copyright (C) 2005 Mark Doliner
006     * Copyright (C) 2005 Grzegorz Lukasik
007     * Copyright (C) 2005 Bj?rn Beskow
008     * Copyright (C) 2006 John Lewis
009     * Copyright (C) 2009 Chris van Es
010     * Copyright (C) 2009 Ed Randall
011     * Copyright (C) 2010 Charlie Squires
012     * Copyright (C) 2010 Piotr Tabor
013     *
014     * Cobertura is free software; you can redistribute it and/or modify
015     * it under the terms of the GNU General Public License as published
016     * by the Free Software Foundation; either version 2 of the License,
017     * or (at your option) any later version.
018     *
019     * Cobertura is distributed in the hope that it will be useful, but
020     * WITHOUT ANY WARRANTY; without even the implied warranty of
021     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
022     * General Public License for more details.
023     *
024     * You should have received a copy of the GNU General Public License
025     * along with Cobertura; if not, write to the Free Software
026     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
027     * USA
028     */
029    
030    package net.sourceforge.cobertura.coveragedata;
031    
032    import java.io.File;
033    import java.util.Collection;
034    import java.util.HashMap;
035    import java.util.Iterator;
036    import java.util.Map;
037    import java.util.SortedSet;
038    import java.util.TreeSet;
039    import java.util.concurrent.locks.Lock;
040    import java.util.concurrent.locks.ReentrantLock;
041    
042    import net.sourceforge.cobertura.util.FileLocker;
043    
044    public class ProjectData extends CoverageDataContainer implements HasBeenInstrumented
045    {
046    
047            private static final long serialVersionUID = 6;
048    
049            /** This collection is used for quicker access to the list of classes. */
050            private Map classes = new HashMap();
051    
052            public void addClassData(ClassData classData)
053            {
054                    lock.lock();
055                    try
056                    {
057                            String packageName = classData.getPackageName();
058                            PackageData packageData = (PackageData)children.get(packageName);
059                            if (packageData == null)
060                            {
061                                    packageData = new PackageData(packageName);
062                                    // Each key is a package name, stored as an String object.
063                                    // Each value is information about the package, stored as a PackageData object.
064                                    this.children.put(packageName, packageData);
065                            }
066                            packageData.addClassData(classData);
067                            this.classes.put(classData.getName(), classData);
068                    }
069                    finally
070                    {
071                            lock.unlock();
072                    }
073            }
074    
075            public ClassData getClassData(String name)
076            {
077                    lock.lock();
078                    try
079                    {
080                            return (ClassData)this.classes.get(name);
081                    }
082                    finally
083                    {
084                            lock.unlock();
085                    }
086            }
087    
088            /**
089             * This is called by instrumented bytecode.
090             */
091            public ClassData getOrCreateClassData(String name)
092            {
093                    lock.lock();
094                    try
095                    {
096                            ClassData classData = (ClassData)this.classes.get(name);
097                            if (classData == null)
098                            {
099                                    classData = new ClassData(name);
100                                    addClassData(classData);
101                            }
102                            return classData;
103                    }
104                    finally
105                    {
106                            lock.unlock();
107                    }
108            }
109    
110            public Collection getClasses()
111            {
112                    lock.lock();
113                    try
114                    {
115                            return this.classes.values();
116                    }
117                    finally
118                    {
119                            lock.unlock();
120                    }
121            }
122    
123            public int getNumberOfClasses()
124            {
125                    lock.lock();
126                    try
127                    {
128                            return this.classes.size();
129                    }
130                    finally
131                    {
132                            lock.unlock();
133                    }
134            }
135    
136            public int getNumberOfSourceFiles()
137            {
138                    return getSourceFiles().size();
139            }
140    
141            public SortedSet getPackages()
142            {
143                    lock.lock();
144                    try
145                    {
146                            return new TreeSet(this.children.values());
147                    }
148                    finally
149                    {
150                            lock.unlock();
151                    }
152            }
153    
154            public Collection getSourceFiles()
155            {
156                    SortedSet sourceFileDatas = new TreeSet();
157                    lock.lock();
158                    try
159                    {
160                            Iterator iter = this.children.values().iterator();
161                            while (iter.hasNext())
162                            {
163                                    PackageData packageData = (PackageData)iter.next();
164                                    sourceFileDatas.addAll(packageData.getSourceFiles());
165                            }
166                    }
167                    finally
168                    {
169                            lock.unlock();
170                    }
171                    return sourceFileDatas;
172            }
173    
174            /**
175             * Get all subpackages of the given package. Includes also specified package if
176             * it exists.
177             *
178             * @param packageName The package name to find subpackages for.
179             *        For example, "com.example"
180             * @return A collection containing PackageData objects.  Each one
181             *         has a name beginning with the given packageName.  For
182             *         example: "com.example.io", "com.example.io.internal"
183             */
184            public SortedSet getSubPackages(String packageName)
185            {
186                    SortedSet subPackages = new TreeSet();
187                    lock.lock();
188                    try
189                    {
190                            Iterator iter = this.children.values().iterator();
191                            while (iter.hasNext())
192                            {
193                                    PackageData packageData = (PackageData)iter.next();
194                                    if (packageData.getName().startsWith(packageName + ".") || packageData.getName().equals(packageName) || packageName.equals(""))
195                                            subPackages.add(packageData);
196                            }
197                    }
198                    finally
199                    {
200                            lock.unlock();
201                    }
202                    return subPackages;
203            }
204    
205            public void merge(CoverageData coverageData)
206            {
207                    if (coverageData == null) {
208                            return;
209                    }
210                    ProjectData projectData = (ProjectData)coverageData;
211                    getBothLocks(projectData);
212                    try
213                    {
214                            super.merge(coverageData);
215            
216                            for (Iterator iter = projectData.classes.keySet().iterator(); iter.hasNext();)
217                            {
218                                    Object key = iter.next();
219                                    if (!this.classes.containsKey(key))
220                                    {
221                                            this.classes.put(key, projectData.classes.get(key));
222                                    }
223                            }
224                    }
225                    finally
226                    {
227                            lock.unlock();
228                            projectData.lock.unlock();
229                    }
230            }
231    
232            // TODO: Is it possible to do this as a static initializer?
233            public static void initialize()
234            {
235                    // Hack for Tomcat - by saving project data right now we force loading
236                    // of classes involved in this process (like ObjectOutputStream)
237                    // so that it won't be necessary to load them on JVM shutdown
238                    if (System.getProperty("catalina.home") != null)
239                    {
240                            saveGlobalProjectData();
241    
242                            // Force the class loader to load some classes that are
243                            // required by our JVM shutdown hook.
244                            // TODO: Use ClassLoader.loadClass("whatever"); instead
245                            ClassData.class.toString();
246                            CoverageData.class.toString();
247                            CoverageDataContainer.class.toString();
248                            FileLocker.class.toString();
249                            HasBeenInstrumented.class.toString();
250                            LineData.class.toString();
251                            PackageData.class.toString();
252                            SourceFileData.class.toString();
253                    }
254    
255                    // Add a hook to save the data when the JVM exits
256                    Runtime.getRuntime().addShutdownHook(new Thread(new SaveTimer()));
257    
258                    // Possibly also save the coverage data every x seconds?
259                    //Timer timer = new Timer(true);
260                    //timer.schedule(saveTimer, 100);
261            }
262    
263            public static void saveGlobalProjectData()
264            {
265                    ProjectData projectDataToSave = new ProjectData();
266                    
267                    TouchCollector.applyTouchesOnProjectData(projectDataToSave);
268    
269    
270                    // Get a file lock
271                    File dataFile = CoverageDataFileHandler.getDefaultDataFile();
272                    
273                    /*
274                     * A note about the next synchronized block:  Cobertura uses static fields to
275                     * hold the data.   When there are multiple classloaders, each classloader
276                     * will keep track of the line counts for the classes that it loads.  
277                     * 
278                     * The static initializers for the Cobertura classes are also called for
279                     * each classloader.   So, there is one shutdown hook for each classloader.
280                     * So, when the JVM exits, each shutdown hook will try to write the
281                     * data it has kept to the datafile.   They will do this at the same
282                     * time.   Before Java 6, this seemed to work fine, but with Java 6, there
283                     * seems to have been a change with how file locks are implemented.   So,
284                     * care has to be taken to make sure only one thread locks a file at a time.
285                     * 
286                     * So, we will synchronize on the string that represents the path to the
287                     * dataFile.  Apparently, there will be only one of these in the JVM
288                     * even if there are multiple classloaders.  I assume that is because
289                     * the String class is loaded by the JVM's root classloader. 
290                     */
291                    synchronized (dataFile.getPath().intern() ) {
292                            FileLocker fileLocker = new FileLocker(dataFile);
293                            
294                            try
295                            {
296                                    // Read the old data, merge our current data into it, then
297                                    // write a new ser file.
298                                    if (fileLocker.lock())
299                                    {
300                                            ProjectData datafileProjectData = loadCoverageDataFromDatafile(dataFile);
301                                            if (datafileProjectData == null)
302                                            {
303                                                    datafileProjectData = projectDataToSave;
304                                            }
305                                            else
306                                            {
307                                                    datafileProjectData.merge(projectDataToSave);
308                                            }
309                                            CoverageDataFileHandler.saveCoverageData(datafileProjectData, dataFile);
310                                    }
311                            }
312                            finally
313                            {
314                                    // Release the file lock
315                                    fileLocker.release();
316                            }
317                    }
318            }
319    
320            private static ProjectData loadCoverageDataFromDatafile(File dataFile)
321            {
322                    ProjectData projectData = null;
323    
324                    // Read projectData from the serialized file.
325                    if (dataFile.isFile())
326                    {
327                            projectData = CoverageDataFileHandler.loadCoverageData(dataFile);
328                    }
329    
330                    if (projectData == null)
331                    {
332                            // We could not read from the serialized file, so use a new object.
333                            System.out.println("Cobertura: Coverage data file " + dataFile.getAbsolutePath()
334                                            + " either does not exist or is not readable.  Creating a new data file.");
335                    }
336    
337                    return projectData;
338            }
339    
340    }