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
028package net.sourceforge.cobertura.coveragedata;
029
030import java.io.File;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.Iterator;
035import java.util.Map;
036import java.util.SortedSet;
037import java.util.TreeSet;
038import java.util.concurrent.locks.Lock;
039import java.util.concurrent.locks.ReentrantLock;
040
041import net.sourceforge.cobertura.util.FileLocker;
042
043public 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}