/* * The Apache Software License, Version 1.1 * * Copyright (c) 2001-2002 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Ant", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact apache@apache.org. * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . */ package org.apache.tools.ant.taskdefs.optional.sitraka; import java.io.File; import java.io.FileInputStream; import java.util.Enumeration; import java.util.Hashtable; import java.util.NoSuchElementException; import java.util.Vector; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.ClassFile; import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.ClassPathLoader; import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.MethodInfo; import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.Utils; /** * Little hack to process XML report from JProbe. It will fix * some reporting errors from JProbe 3.0 and makes use of a reference * classpath to add classes/methods that were not reported by JProbe * as being used (ie loaded) * * @author Stephane Bailliez */ public class XMLReport { /** task caller, can be null, used for logging purpose */ private Task task; /** the XML file to process just from CovReport */ private File file; /** jprobe home path. It is used to get the DTD */ private File jprobeHome; /** parsed document */ private Document report; /** mapping of class names to ClassFiles from the reference classpath. It is used to filter the JProbe report. */ private Hashtable classFiles; /** mapping package name / package node for faster access */ private Hashtable pkgMap; /** mapping classname / class node for faster access */ private Hashtable classMap; /** method filters */ private ReportFilters filters; /** create a new XML report, logging will be on stdout */ public XMLReport(File file) { this(null, file); } /** create a new XML report, logging done on the task */ public XMLReport(Task task, File file) { this.file = file; this.task = task; } /** set the JProbe home path. Used to get the DTD */ public void setJProbehome(File home) { jprobeHome = home; } /** set the */ public void setReportFilters(ReportFilters filters) { this.filters = filters; } /** create node maps so that we can access node faster by their name */ protected void createNodeMaps() { pkgMap = new Hashtable(); classMap = new Hashtable(); // create a map index of all packages by their name // @todo can be done faster by direct access. NodeList packages = report.getElementsByTagName("package"); final int pkglen = packages.getLength(); log("Indexing " + pkglen + " packages"); for (int i = pkglen - 1; i > -1; i--) { Element pkg = (Element) packages.item(i); String pkgname = pkg.getAttribute("name"); int nbclasses = 0; // create a map index of all classes by their fully // qualified name. NodeList classes = pkg.getElementsByTagName("class"); final int classlen = classes.getLength(); log("Indexing " + classlen + " classes in package " + pkgname); for (int j = classlen - 1; j > -1; j--) { Element clazz = (Element) classes.item(j); String classname = clazz.getAttribute("name"); if (pkgname != null && pkgname.length() != 0) { classname = pkgname + "." + classname; } int nbmethods = 0; NodeList methods = clazz.getElementsByTagName("method"); final int methodlen = methods.getLength(); for (int k = methodlen - 1; k > -1; k--) { Element meth = (Element) methods.item(k); StringBuffer methodname = new StringBuffer(meth.getAttribute("name")); methodname.delete(methodname.toString().indexOf("("), methodname.toString().length()); String signature = classname + "." + methodname + "()"; if (filters.accept(signature)) { log("kept method:" + signature); nbmethods++; } else { clazz.removeChild(meth); } } // if we don't keep any method, we don't keep the class if (nbmethods != 0 && classFiles.containsKey(classname)) { log("Adding class '" + classname + "'"); classMap.put(classname, clazz); nbclasses++; } else { pkg.removeChild(clazz); } } if (nbclasses != 0) { log("Adding package '" + pkgname + "'"); pkgMap.put(pkgname, pkg); } else { pkg.getParentNode().removeChild(pkg); } } log("Indexed " + classMap.size() + " classes in " + pkgMap.size() + " packages"); } /** create the whole new document */ public Document createDocument(String[] classPath) throws Exception { // Iterate over the classpath to identify reference classes classFiles = new Hashtable(); ClassPathLoader cpl = new ClassPathLoader(classPath); Enumeration enum = cpl.loaders(); while (enum.hasMoreElements()) { ClassPathLoader.FileLoader fl = (ClassPathLoader.FileLoader) enum.nextElement(); ClassFile[] classes = fl.getClasses(); log("Processing " + classes.length + " classes in " + fl.getFile()); // process all classes for (int i = 0; i < classes.length; i++) { classFiles.put(classes[i].getFullName(), classes[i]); } } // Load the JProbe coverage XML report DocumentBuilder dbuilder = newBuilder(); InputSource is = new InputSource(new FileInputStream(file)); if (jprobeHome != null) { File dtdDir = new File(jprobeHome, "dtd"); is.setSystemId("file:///" + dtdDir.getAbsolutePath() + "/"); } report = dbuilder.parse(is); report.normalize(); // create maps for faster node access (also filters out unwanted nodes) createNodeMaps(); // Make sure each class from the reference path ends up in the report Enumeration classes = classFiles.elements(); while (classes.hasMoreElements()) { ClassFile cf = (ClassFile) classes.nextElement(); serializeClass(cf); } // update the document with the stats update(); return report; } /** * JProbe does not put the java.lang prefix for classes * in this package, so used this nice method so that * I have the same signature for methods */ protected String getMethodSignature(MethodInfo method) { StringBuffer buf = new StringBuffer(method.getName()); buf.append("("); String[] params = method.getParametersType(); for (int i = 0; i < params.length; i++) { String type = params[i]; int pos = type.lastIndexOf('.'); if (pos != -1) { String pkg = type.substring(0, pos); if ("java.lang".equals(pkg)) { params[i] = type.substring(pos + 1); } } buf.append(params[i]); if (i != params.length - 1) { buf.append(", "); } } buf.append(")"); return buf.toString(); } /** * Convert to a CovReport-like signature - <classname>.<method>(). */ protected String getMethodSignature(ClassFile clazz, MethodInfo method) { StringBuffer buf = new StringBuffer(clazz.getFullName()); buf.append("."); buf.append(method.getName()); buf.append("()"); return buf.toString(); } /** * Do additional work on an element to remove abstract methods that * are reported by JProbe 3.0 */ protected void removeAbstractMethods(ClassFile classFile, Element classNode) { MethodInfo[] methods = classFile.getMethods(); Hashtable methodNodeList = getMethods(classNode); // assert xmlMethods.size() == methods.length() final int size = methods.length; for (int i = 0; i < size; i++) { MethodInfo method = methods[i]; String methodSig = getMethodSignature(method); Element methodNode = (Element) methodNodeList.get(methodSig); if (methodNode != null && Utils.isAbstract(method.getAccessFlags())) { log("\tRemoving abstract method " + methodSig); classNode.removeChild(methodNode); } } } /** create an empty method element with its cov.data values */ protected Element createMethodElement(MethodInfo method) { String methodsig = getMethodSignature(method); Element methodElem = report.createElement("method"); methodElem.setAttribute("name", methodsig); // create the method cov.data element Element methodData = report.createElement("cov.data"); methodElem.appendChild(methodData); methodData.setAttribute("calls", "0"); methodData.setAttribute("hit_lines", "0"); methodData.setAttribute("total_lines", String.valueOf(method.getNumberOfLines())); return methodElem; } /** create an empty package element with its default cov.data (0) */ protected Element createPackageElement(String pkgname) { Element pkgElem = report.createElement("package"); pkgElem.setAttribute("name", pkgname); // create the package cov.data element / default // must be updated at the end of the whole process Element pkgData = report.createElement("cov.data"); pkgElem.appendChild(pkgData); pkgData.setAttribute("calls", "0"); pkgData.setAttribute("hit_methods", "0"); pkgData.setAttribute("total_methods", "0"); pkgData.setAttribute("hit_lines", "0"); pkgData.setAttribute("total_lines", "0"); return pkgElem; } /** create an empty class element with its default cov.data (0) */ protected Element createClassElement(ClassFile classFile) { // create the class element Element classElem = report.createElement("class"); classElem.setAttribute("name", classFile.getName()); // source file possibly does not exist in the bytecode if (null != classFile.getSourceFile()) { classElem.setAttribute("source", classFile.getSourceFile()); } // create the cov.data elem Element classData = report.createElement("cov.data"); classElem.appendChild(classData); // create the class cov.data element classData.setAttribute("calls", "0"); classData.setAttribute("hit_methods", "0"); classData.setAttribute("total_methods", "0"); classData.setAttribute("hit_lines", "0"); classData.setAttribute("total_lines", "0"); return classElem; } /** serialize a classfile into XML */ protected void serializeClass(ClassFile classFile) { // the class already is reported so ignore it String fullclassname = classFile.getFullName(); log("Looking for '" + fullclassname + "'"); Element clazz = (Element) classMap.get(fullclassname); // ignore classes that are already reported, all the information is // already there. if (clazz != null) { log("Ignoring " + fullclassname); removeAbstractMethods(classFile, clazz); return; } // ignore interfaces files, there is no code in there to cover. if (Utils.isInterface(classFile.getAccess())) { return; } Vector methods = getFilteredMethods(classFile); // no need to process, there are no methods to add for this class. if (methods.size() == 0) { return; } String pkgname = classFile.getPackage(); // System.out.println("Looking for package " + pkgname); Element pkgElem = (Element) pkgMap.get(pkgname); if (pkgElem == null) { pkgElem = createPackageElement(pkgname); report.getDocumentElement().appendChild(pkgElem); pkgMap.put(pkgname, pkgElem); // add the pkg to the map } // this is a brand new class, so we have to create a new node // create the class element Element classElem = createClassElement(classFile); pkgElem.appendChild(classElem); int total_lines = 0; int total_methods = 0; final int count = methods.size(); for (int i = 0; i < count; i++) { // create the method element MethodInfo method = (MethodInfo) methods.elementAt(i); if (Utils.isAbstract(method.getAccessFlags())) { continue; // no need to report abstract methods } Element methodElem = createMethodElement(method); classElem.appendChild(methodElem); total_lines += method.getNumberOfLines(); total_methods++; } // create the class cov.data element Element classData = getCovDataChild(classElem); classData.setAttribute("total_methods", String.valueOf(total_methods)); classData.setAttribute("total_lines", String.valueOf(total_lines)); // add itself to the node map classMap.put(fullclassname, classElem); } protected Vector getFilteredMethods(ClassFile classFile) { MethodInfo[] methodlist = classFile.getMethods(); Vector methods = new Vector(methodlist.length); for (int i = 0; i < methodlist.length; i++) { MethodInfo method = methodlist[i]; String signature = getMethodSignature(classFile, method); if (filters.accept(signature)) { methods.addElement(method); log("keeping " + signature); } else { // log("discarding " + signature); } } return methods; } /** update the count of the XML, that is accumulate the stats on * methods, classes and package so that the numbers are valid * according to the info that was appended to the XML. */ protected void update() { int calls = 0; int hit_methods = 0; int total_methods = 0; int hit_lines = 0; int total_lines = 0; // use the map for access, all nodes should be there Enumeration enum = pkgMap.elements(); while (enum.hasMoreElements()) { Element pkgElem = (Element) enum.nextElement(); String pkgname = pkgElem.getAttribute("name"); Element[] classes = getClasses(pkgElem); int pkg_calls = 0; int pkg_hit_methods = 0; int pkg_total_methods = 0; int pkg_hit_lines = 0; int pkg_total_lines = 0; //System.out.println("Processing package '" + pkgname + "': " + classes.length + " classes"); for (int j = 0; j < classes.length; j++) { Element clazz = classes[j]; String classname = clazz.getAttribute("name"); if (pkgname != null && pkgname.length() != 0) { classname = pkgname + "." + classname; } // there's only cov.data as a child so bet on it Element covdata = getCovDataChild(clazz); try { pkg_calls += Integer.parseInt(covdata.getAttribute("calls")); pkg_hit_methods += Integer.parseInt(covdata.getAttribute("hit_methods")); pkg_total_methods += Integer.parseInt(covdata.getAttribute("total_methods")); pkg_hit_lines += Integer.parseInt(covdata.getAttribute("hit_lines")); pkg_total_lines += Integer.parseInt(covdata.getAttribute("total_lines")); } catch (NumberFormatException e) { log("Error parsing '" + classname + "' (" + j + "/" + classes.length + ") in package '" + pkgname + "'"); throw e; } } Element covdata = getCovDataChild(pkgElem); covdata.setAttribute("calls", String.valueOf(pkg_calls)); covdata.setAttribute("hit_methods", String.valueOf(pkg_hit_methods)); covdata.setAttribute("total_methods", String.valueOf(pkg_total_methods)); covdata.setAttribute("hit_lines", String.valueOf(pkg_hit_lines)); covdata.setAttribute("total_lines", String.valueOf(pkg_total_lines)); calls += pkg_calls; hit_methods += pkg_hit_methods; total_methods += pkg_total_methods; hit_lines += pkg_hit_lines; total_lines += pkg_total_lines; } Element covdata = getCovDataChild(report.getDocumentElement()); covdata.setAttribute("calls", String.valueOf(calls)); covdata.setAttribute("hit_methods", String.valueOf(hit_methods)); covdata.setAttribute("total_methods", String.valueOf(total_methods)); covdata.setAttribute("hit_lines", String.valueOf(hit_lines)); covdata.setAttribute("total_lines", String.valueOf(total_lines)); } protected Element getCovDataChild(Element parent) { NodeList children = parent.getChildNodes(); int len = children.getLength(); for (int i = 0; i < len; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) child; if ("cov.data".equals(elem.getNodeName())) { return elem; } } } throw new NoSuchElementException("Could not find 'cov.data' element in parent '" + parent.getNodeName() + "'"); } protected Hashtable getMethods(Element clazz) { Hashtable map = new Hashtable(); NodeList children = clazz.getChildNodes(); int len = children.getLength(); for (int i = 0; i < len; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) child; if ("method".equals(elem.getNodeName())) { String name = elem.getAttribute("name"); map.put(name, elem); } } } return map; } protected Element[] getClasses(Element pkg) { Vector v = new Vector(); NodeList children = pkg.getChildNodes(); int len = children.getLength(); for (int i = 0; i < len; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) child; if ("class".equals(elem.getNodeName())) { v.addElement(elem); } } } Element[] elems = new Element[v.size()]; v.copyInto(elems); return elems; } protected Element[] getPackages(Element snapshot) { Vector v = new Vector(); NodeList children = snapshot.getChildNodes(); int len = children.getLength(); for (int i = 0; i < len; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) child; if ("package".equals(elem.getNodeName())) { v.addElement(elem); } } } Element[] elems = new Element[v.size()]; v.copyInto(elems); return elems; } private static DocumentBuilder newBuilder() { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setIgnoringComments(true); factory.setValidating(false); return factory.newDocumentBuilder(); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public void log(String message) { if (task == null) { //System.out.println(message); } else { task.log(message, Project.MSG_DEBUG); } } }