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}