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
030package net.sourceforge.cobertura.coveragedata;
031
032import java.io.File;
033
034import java.util.Collection;
035import java.util.HashMap;
036import java.util.Iterator;
037import java.util.Map;
038import java.util.SortedSet;
039import java.util.TreeSet;
040import java.util.concurrent.locks.Lock;
041import java.util.concurrent.locks.ReentrantLock;
042import java.util.logging.Logger;
043
044import net.sourceforge.cobertura.CoverageIgnore;
045import net.sourceforge.cobertura.util.FileLocker;
046
047@CoverageIgnore
048public class ProjectData extends CoverageDataContainer {
049    private static final Logger logger = Logger.getLogger(ProjectData.class.getCanonicalName());
050        private static final long serialVersionUID = 6;
051
052        private static ProjectData globalProjectData = null;
053
054        private static Thread shutdownHook;
055        private static final transient Lock globalProjectDataLock = new ReentrantLock();
056
057        /** This collection is used for quicker access to the list of classes. */
058        private Map classes = new HashMap();
059
060        public void addClassData(ClassData classData)
061        {
062                lock.lock();
063                try
064                {
065                        String packageName = classData.getPackageName();
066                        PackageData packageData = (PackageData)children.get(packageName);
067                        if (packageData == null)
068                        {
069                                packageData = new PackageData(packageName);
070                                // Each key is a package name, stored as an String object.
071                                // Each value is information about the package, stored as a PackageData object.
072                                this.children.put(packageName, packageData);
073                        }
074                        packageData.addClassData(classData);
075                        this.classes.put(classData.getName(), classData);
076                }
077                finally
078                {
079                        lock.unlock();
080                }
081        }
082
083        public ClassData getClassData(String name)
084        {
085                return (ClassData)this.classes.get(name);
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 + ".") 
195                                        || packageData.getName().equals(packageName)
196                                        || (packageName.length() == 0)) {                                       
197                                  subPackages.add(packageData);
198                                }
199                        }
200                }
201                finally
202                {
203                        lock.unlock();
204                }
205                return subPackages;
206        }
207
208        public void merge(CoverageData coverageData)
209        {
210                if (coverageData == null) {
211                        return;
212                }
213                ProjectData projectData = (ProjectData)coverageData;
214                getBothLocks(projectData);
215                try
216                {
217                        super.merge(coverageData);
218        
219                        for (Iterator iter = projectData.classes.keySet().iterator(); iter.hasNext();)
220                        {
221                                Object key = iter.next();
222                                if (!this.classes.containsKey(key))
223                                {
224                                        this.classes.put(key, projectData.classes.get(key));
225                                }
226                        }
227                }
228                finally
229                {
230                        lock.unlock();
231                        projectData.lock.unlock();
232                }
233        }
234
235        /**
236         * Get a reference to a ProjectData object in order to increase the
237         * coverage count for a specific line.
238         *
239         * This method is only called by code that has been instrumented.  It
240         * is not called by any of the Cobertura code or ant tasks.
241         */
242        public static ProjectData getGlobalProjectData()
243        {
244                globalProjectDataLock.lock();
245                try
246                {
247                        if (globalProjectData != null)
248                                return globalProjectData;
249        
250                        globalProjectData = new ProjectData();
251                        initialize();
252                        return globalProjectData;
253                }
254                finally
255                {
256                        globalProjectDataLock.unlock();
257                }
258        }
259
260        // TODO: Is it possible to do this as a static initializer?
261        private static void initialize()
262        {
263                // Hack for Tomcat - by saving project data right now we force loading
264                // of classes involved in this process (like ObjectOutputStream)
265                // so that it won't be necessary to load them on JVM shutdown
266                if (System.getProperty("catalina.home") != null)
267                {
268                        saveGlobalProjectData();
269
270                        // Force the class loader to load some classes that are
271                        // required by our JVM shutdown hook.
272                        // TODO: Use ClassLoader.loadClass("whatever"); instead
273                        ClassData.class.toString();
274                        CoverageData.class.toString();
275                        CoverageDataContainer.class.toString();
276                        FileLocker.class.toString();
277                        LineData.class.toString();
278                        PackageData.class.toString();
279                        SourceFileData.class.toString();
280                }
281
282                // Add a hook to save the data when the JVM exits
283                shutdownHook=new Thread(new SaveTimer());
284                Runtime.getRuntime().addShutdownHook(shutdownHook);     
285                // Possibly also save the coverage data every x seconds?
286                //Timer timer = new Timer(true);
287                //timer.schedule(saveTimer, 100);
288        }
289
290        public static void saveGlobalProjectData()
291        {
292                ProjectData projectDataToSave = null;
293                
294                globalProjectDataLock.lock();
295                try
296                {
297                        projectDataToSave = getGlobalProjectData();                                             
298        
299                        /*
300                         * The next statement is not necessary at the moment, because this method is only called
301                         * either at the very beginning or at the very end of a test.  If the code is changed
302                         * to save more frequently, then this will become important.
303                         */
304                        globalProjectData = new ProjectData();
305                }
306                finally
307                {
308                        globalProjectDataLock.unlock();
309                }
310
311                /*
312                 * Now sleep a bit in case there is a thread still holding a reference to the "old"
313                 * globalProjectData (now referenced with projectDataToSave).  
314                 * We want it to finish its updates.  I assume 1 second is plenty of time.
315                 */
316                try
317                {
318                        Thread.sleep(1000);
319                }
320                catch (InterruptedException e)
321                {
322                }
323                
324                TouchCollector.applyTouchesOnProjectData(projectDataToSave);
325
326
327                // Get a file lock
328                File dataFile = CoverageDataFileHandler.getDefaultDataFile();           
329                /*
330                 * A note about the next synchronized block:  Cobertura uses static fields to
331                 * hold the data.   When there are multiple classloaders, each classloader
332                 * will keep track of the line counts for the classes that it loads.  
333                 * 
334                 * The static initializers for the Cobertura classes are also called for
335                 * each classloader.   So, there is one shutdown hook for each classloader.
336                 * So, when the JVM exits, each shutdown hook will try to write the
337                 * data it has kept to the datafile.   They will do this at the same
338                 * time.   Before Java 6, this seemed to work fine, but with Java 6, there
339                 * seems to have been a change with how file locks are implemented.   So,
340                 * care has to be taken to make sure only one thread locks a file at a time.
341                 * 
342                 * So, we will synchronize on the string that represents the path to the
343                 * dataFile.  Apparently, there will be only one of these in the JVM
344                 * even if there are multiple classloaders.  I assume that is because
345                 * the String class is loaded by the JVM's root classloader. 
346                 */
347                synchronized (dataFile.getPath().intern() ) {
348                        FileLocker fileLocker = new FileLocker(dataFile);
349                        
350                        try
351                        {
352                                // Read the old data, merge our current data into it, then
353                                // write a new ser file.
354                                if (fileLocker.lock())
355                                {
356                                        ProjectData datafileProjectData = loadCoverageDataFromDatafile(dataFile);
357                                        if (datafileProjectData == null)
358                                        {
359                                                datafileProjectData = projectDataToSave;
360                                        }
361                                        else
362                                        {
363                                                datafileProjectData.merge(projectDataToSave);
364                                        }
365                                        CoverageDataFileHandler.saveCoverageData(datafileProjectData, dataFile);
366                                }
367                        }
368                        finally
369                        {
370                                // Release the file lock
371                                fileLocker.release();
372                        }
373                }
374        }
375        
376        public static void turnOffAutoSave(){
377                if (shutdownHook!=null){
378                        Runtime.getRuntime().removeShutdownHook(shutdownHook);
379                }
380        }
381
382        private static ProjectData loadCoverageDataFromDatafile(File dataFile)
383        {
384                ProjectData projectData = null;
385
386                // Read projectData from the serialized file.
387                if (dataFile.isFile())
388                {
389                        projectData = CoverageDataFileHandler.loadCoverageData(dataFile);
390                }
391
392                if (projectData == null)
393                {
394                        // We could not read from the serialized file, so use a new object.
395                        logger.info("Cobertura: Coverage data file " + dataFile.getAbsolutePath()
396                                        + " either does not exist or is not readable.  Creating a new data file.");
397                }
398
399                return projectData;
400        }
401
402}