001/******************************************************************************* 002 * Copyright (c) 2017, 2018 Red Hat Inc and others. 003 * All rights reserved. This program and the accompanying materials 004 * are made available under the terms of the Eclipse Public License v1.0 005 * which accompanies this distribution, and is available at 006 * http://www.eclipse.org/legal/epl-v10.html 007 * 008 * Contributors: 009 * Jens Reimann - initial API and implementation 010 *******************************************************************************/ 011package de.dentrassi.maven.jacoco; 012 013import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 014import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE; 015import static org.apache.maven.artifact.Artifact.SCOPE_PROVIDED; 016import static org.apache.maven.artifact.Artifact.SCOPE_RUNTIME; 017import static org.apache.maven.artifact.Artifact.SCOPE_TEST; 018import static org.apache.maven.plugins.annotations.LifecyclePhase.VERIFY; 019 020import java.io.BufferedInputStream; 021import java.io.BufferedOutputStream; 022import java.io.File; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.util.Arrays; 029import java.util.HashSet; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Set; 033 034import javax.xml.parsers.DocumentBuilderFactory; 035import javax.xml.transform.OutputKeys; 036import javax.xml.transform.Transformer; 037import javax.xml.transform.TransformerFactory; 038import javax.xml.transform.dom.DOMSource; 039import javax.xml.transform.stream.StreamResult; 040 041import org.apache.maven.model.Dependency; 042import org.apache.maven.plugin.AbstractMojo; 043import org.apache.maven.plugin.MojoExecutionException; 044import org.apache.maven.plugin.MojoFailureException; 045import org.apache.maven.plugins.annotations.Mojo; 046import org.apache.maven.plugins.annotations.Parameter; 047import org.apache.maven.plugins.annotations.ResolutionScope; 048import org.apache.maven.project.MavenProject; 049import org.jacoco.report.IReportGroupVisitor; 050import org.jacoco.report.IReportVisitor; 051import org.w3c.dom.Document; 052 053/** 054 * Convert binary execution data to XML report including dependencies. <br> 055 * This mojo will convert the binary jacoco execution data into the same format 056 * as the XML report. But it will take all module dependencies into 057 * consideration. This may be helpful when tools actually require XML data, but 058 * tests in a module are responsible for testing classes in other modules. 059 */ 060@Mojo(defaultPhase = VERIFY, name = "xml", requiresProject = true, inheritByDefault = true, requiresDependencyResolution = ResolutionScope.TEST) 061public class XmlMojo extends AbstractMojo { 062 063 private final static String PROP_PREFIX = "jacoco.extras."; 064 065 /** 066 * Allows to skip the execution 067 */ 068 @Parameter(property = PROP_PREFIX + "skip", defaultValue = "false") 069 private boolean skip; 070 071 /** 072 * The jacoco execution data <br> 073 * If this file doesn't exist, execution of this plugin will be skipped 074 */ 075 @Parameter(property = PROP_PREFIX 076 + "execFile", defaultValue = "${project.build.directory}/jacoco.exec", required = true) 077 private File execFile; 078 079 /** 080 * The output XML file 081 */ 082 @Parameter(property = PROP_PREFIX 083 + "xmlFile", defaultValue = "${project.build.directory}/jacoco.xml", required = true) 084 private File xmlFile; 085 086 /** 087 * The encoding of the source files 088 */ 089 @Parameter(property = "project.build.sourceEncoding", defaultValue = "UTF-8") 090 private String sourceEncoding; 091 092 /** 093 * Include patterns. The default is to include everything. 094 */ 095 @Parameter 096 private List<String> includes; 097 098 /** 099 * Exclude patterns. The default is to exclude nothing. 100 */ 101 @Parameter 102 private List<String> excludes; 103 104 @Parameter(property = "project", readonly = true) 105 private MavenProject project; 106 107 @Parameter(property = "reactorProjects", readonly = true) 108 private List<MavenProject> reactorProjects; 109 110 /** 111 * If the XML file should be pretty printed. 112 * 113 * @since 0.1.2 114 */ 115 @Parameter(property = PROP_PREFIX + "pretty", defaultValue = "true") 116 private boolean pretty = true; 117 118 /** 119 * When pretty printing, if the original file should be deleted. 120 * 121 * @since 0.1.2 122 */ 123 @Parameter(property = PROP_PREFIX + "deleteRaw", defaultValue = "true") 124 private boolean deleteRaw = true; 125 126 /** 127 * Scopes to consider for dependencies. 128 */ 129 @Parameter(property = PROP_PREFIX + "scopes", defaultValue = "compile,runtime,provided,test") 130 private String[] scopes = new String[] { SCOPE_COMPILE, SCOPE_RUNTIME, SCOPE_PROVIDED, SCOPE_TEST }; 131 132 /** 133 * Process transient dependencies. 134 */ 135 @Parameter(property = PROP_PREFIX + "transientDependencies", defaultValue = "true") 136 private boolean transientDependencies = true; 137 138 public void setTransientDependencies(boolean transientDependencies) { 139 this.transientDependencies = transientDependencies; 140 } 141 142 public void setScopes(String[] scopes) { 143 this.scopes = scopes; 144 } 145 146 public void setPretty(final boolean pretty) { 147 this.pretty = pretty; 148 } 149 150 public void setDeleteRaw(boolean deleteRaw) { 151 this.deleteRaw = deleteRaw; 152 } 153 154 @Override 155 public void execute() throws MojoExecutionException, MojoFailureException { 156 if (this.skip) { 157 return; 158 } 159 160 if (!this.execFile.isFile()) { 161 getLog().debug("Not running. No execution data found."); 162 return; 163 } 164 165 try { 166 this.xmlFile.getParentFile().mkdirs(); 167 168 final ReportSupport report = new ReportSupport(getLog()); 169 report.loadExecutionData(this.execFile); 170 report.addXmlFormatter(this.xmlFile, "UTF-8"); 171 172 final IReportVisitor visitor = report.initRootVisitor(); 173 final IReportGroupVisitor group = visitor.visitGroup("XML"); 174 175 processProject(report, group, this.project); 176 177 for (final MavenProject dependency : findDependencies(this.scopes)) { 178 processProject(report, group, dependency); 179 } 180 181 visitor.visitEnd(); 182 183 if (this.pretty) { 184 makePretty(); 185 } 186 187 } catch (final IOException e) { 188 throw new MojoExecutionException("Failed to convert to XML", e); 189 } 190 } 191 192 private void processProject(final ReportSupport report, final IReportGroupVisitor group, final MavenProject project) 193 throws IOException { 194 report.processProject(group, project.getArtifactId(), project, this.includes, this.excludes, 195 this.sourceEncoding); 196 } 197 198 private void makePretty() throws MojoExecutionException { 199 try { 200 final TransformerFactory tf = TransformerFactory.newInstance(); 201 202 final Transformer transformer = tf.newTransformer(); 203 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 204 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 205 transformer.setOutputProperty(OutputKeys.ENCODING, this.sourceEncoding); 206 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//JACOCO//DTD Report 1.0//EN"); 207 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "report.dtd"); 208 209 final Path tmp = this.xmlFile.toPath().getParent() 210 .resolve("raw." + this.xmlFile.toPath().getFileName().toString()); 211 212 Files.move(this.xmlFile.toPath(), tmp, REPLACE_EXISTING); 213 214 try (final InputStream in = new BufferedInputStream(Files.newInputStream(tmp)); 215 final OutputStream out = new BufferedOutputStream(Files.newOutputStream(this.xmlFile.toPath()));) { 216 217 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 218 dbf.setValidating(false); 219 dbf.setExpandEntityReferences(false); 220 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 221 final Document doc = dbf.newDocumentBuilder().parse(in); 222 223 final DOMSource source = new DOMSource(doc); 224 final StreamResult result = new StreamResult(out); 225 226 transformer.transform(source, result); 227 } 228 229 // we only delete this if we succeeded 230 if (this.deleteRaw) { 231 Files.deleteIfExists(tmp); 232 } 233 234 } catch (final Exception e) { 235 throw new MojoExecutionException("Failed to pretty print XML file", e); 236 } 237 238 } 239 240 private List<MavenProject> findDependencies(final String... scopes) { 241 242 final Set<String> knownDependencies = new HashSet<>(); 243 final List<MavenProject> result = new LinkedList<>(); 244 245 findDependencies(new HashSet<>(Arrays.asList(scopes)), result, knownDependencies, project.getDependencies()); 246 247 return result; 248 249 } 250 251 private void findDependencies(final Set<String> scopes, final List<MavenProject> result, 252 final Set<String> knownDependencies, List<Dependency> dependencies) { 253 254 for (final Dependency dependency : dependencies) { 255 256 if (!scopes.contains(dependency.getScope())) { 257 continue; 258 } 259 260 final String key = toKey(dependency); 261 if (!knownDependencies.add(key)) { 262 continue; 263 } 264 265 getLog().debug("Adding dependency - " + dependency.toString()); 266 final MavenProject project = findProjectFromReactor(dependency); 267 if (project != null) { 268 result.add(project); 269 if (this.transientDependencies) { 270 findDependencies(scopes, result, knownDependencies, project.getDependencies()); 271 } 272 } else { 273 getLog().debug(" -> Unable to find in reactor"); 274 } 275 } 276 } 277 278 private static String toKey(final Dependency dependency) { 279 return dependency.getManagementKey() + dependency.getVersion(); 280 } 281 282 private MavenProject findProjectFromReactor(final Dependency d) { 283 for (final MavenProject p : this.reactorProjects) { 284 if (p.getGroupId().equals(d.getGroupId()) && p.getArtifactId().equals(d.getArtifactId()) 285 && p.getVersion().equals(d.getVersion())) { 286 return p; 287 } 288 } 289 return null; 290 } 291 292}