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 }