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}