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