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