1/*******************************************************************************
2 * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *    Evgeny Mandrikov - initial API and implementation
10 *    Kyle Lieber - implementation of CheckMojo
11 *
12 *******************************************************************************/
13package org.jacoco.maven;
14
15import static java.lang.String.format;
16
17import java.io.File;
18import java.io.FileInputStream;
19import java.io.FileOutputStream;
20import java.io.IOException;
21import java.io.InputStreamReader;
22import java.io.Reader;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.List;
26import java.util.Locale;
27
28import org.apache.maven.plugin.logging.Log;
29import org.apache.maven.project.MavenProject;
30import org.jacoco.core.analysis.Analyzer;
31import org.jacoco.core.analysis.CoverageBuilder;
32import org.jacoco.core.analysis.IBundleCoverage;
33import org.jacoco.core.analysis.IClassCoverage;
34import org.jacoco.core.tools.ExecFileLoader;
35import org.jacoco.report.FileMultiReportOutput;
36import org.jacoco.report.IReportGroupVisitor;
37import org.jacoco.report.IReportVisitor;
38import org.jacoco.report.ISourceFileLocator;
39import org.jacoco.report.MultiReportVisitor;
40import org.jacoco.report.check.IViolationsOutput;
41import org.jacoco.report.check.Rule;
42import org.jacoco.report.check.RulesChecker;
43import org.jacoco.report.csv.CSVFormatter;
44import org.jacoco.report.html.HTMLFormatter;
45import org.jacoco.report.xml.XMLFormatter;
46
47/**
48 * Encapsulates the tasks to create reports for Maven projects. Instances are
49 * supposed to be used in the following sequence:
50 *
51 * <ol>
52 * <li>Create an instance</li>
53 * <li>Load one or multiple exec files with <code>loadExecutionData()</code></li>
54 * <li>Add one or multiple formatters with <code>addXXX()</code> methods</li>
55 * <li>Create the root visitor with <code>initRootVisitor()</code></li>
56 * <li>Process one or multiple projects with <code>processProject()</code></li>
57 * </ol>
58 */
59final class ReportSupport {
60
61	private final Log log;
62	private final ExecFileLoader loader;
63	private final List<IReportVisitor> formatters;
64
65	/**
66	 * Construct a new instance with the given log output.
67	 *
68	 * @param log
69	 *            for log output
70	 */
71	public ReportSupport(final Log log) {
72		this.log = log;
73		this.loader = new ExecFileLoader();
74		this.formatters = new ArrayList<IReportVisitor>();
75	}
76
77	/**
78	 * Loads the given execution data file.
79	 *
80	 * @param execFile
81	 *            execution data file to load
82	 * @throws IOException
83	 *             if the file can't be loaded
84	 */
85	public void loadExecutionData(final File execFile) throws IOException {
86		log.info("Loading execution data file " + execFile);
87		loader.load(execFile);
88	}
89
90	public void addXmlFormatter(final File targetfile, final String encoding)
91			throws IOException {
92		final XMLFormatter xml = new XMLFormatter();
93		xml.setOutputEncoding(encoding);
94		formatters.add(xml.createVisitor(new FileOutputStream(targetfile)));
95	}
96
97	public void addCsvFormatter(final File targetfile, final String encoding)
98			throws IOException {
99		final CSVFormatter csv = new CSVFormatter();
100		csv.setOutputEncoding(encoding);
101		formatters.add(csv.createVisitor(new FileOutputStream(targetfile)));
102	}
103
104	public void addHtmlFormatter(final File targetdir, final String encoding,
105			final String footer, final Locale locale) throws IOException {
106		final HTMLFormatter htmlFormatter = new HTMLFormatter();
107		htmlFormatter.setOutputEncoding(encoding);
108		htmlFormatter.setLocale(locale);
109		if (footer != null) {
110			htmlFormatter.setFooterText(footer);
111		}
112		formatters.add(htmlFormatter.createVisitor(new FileMultiReportOutput(
113				targetdir)));
114	}
115
116	public void addAllFormatters(final File targetdir, final String encoding,
117			final String footer, final Locale locale) throws IOException {
118		targetdir.mkdirs();
119		addXmlFormatter(new File(targetdir, "jacoco.xml"), encoding);
120		addCsvFormatter(new File(targetdir, "jacoco.csv"), encoding);
121		addHtmlFormatter(targetdir, encoding, footer, locale);
122	}
123
124	public void addRulesChecker(final List<Rule> rules,
125			final IViolationsOutput output) {
126		final RulesChecker checker = new RulesChecker();
127		checker.setRules(rules);
128		formatters.add(checker.createVisitor(output));
129	}
130
131	public IReportVisitor initRootVisitor() throws IOException {
132		final IReportVisitor visitor = new MultiReportVisitor(formatters);
133		visitor.visitInfo(loader.getSessionInfoStore().getInfos(), loader
134				.getExecutionDataStore().getContents());
135		return visitor;
136	}
137
138	/**
139	 * Calculates coverage for the given project and emits it to the report
140	 * group without source references
141	 *
142	 * @param visitor
143	 *            group visitor to emit the project's coverage to
144	 * @param project
145	 *            the MavenProject
146	 * @param includes
147	 *            list of includes patterns
148	 * @param excludes
149	 *            list of excludes patterns
150	 * @throws IOException
151	 *             if class files can't be read
152	 */
153	public void processProject(final IReportGroupVisitor visitor,
154			final MavenProject project, final List<String> includes,
155			final List<String> excludes) throws IOException {
156		processProject(visitor, project.getArtifactId(), project, includes,
157				excludes, new NoSourceLocator());
158	}
159
160	/**
161	 * Calculates coverage for the given project and emits it to the report
162	 * group including source references
163	 *
164	 * @param visitor
165	 *            group visitor to emit the project's coverage to
166	 * @param bundeName
167	 *            name for this project in the report
168	 * @param project
169	 *            the MavenProject
170	 * @param includes
171	 *            list of includes patterns
172	 * @param excludes
173	 *            list of excludes patterns
174	 * @param srcEncoding
175	 *            encoding of the source files within this project
176	 * @throws IOException
177	 *             if class files can't be read
178	 */
179	public void processProject(final IReportGroupVisitor visitor,
180			final String bundeName, final MavenProject project,
181			final List<String> includes, final List<String> excludes,
182			final String srcEncoding) throws IOException {
183		processProject(visitor, bundeName, project, includes, excludes,
184				new SourceFileCollection(project, srcEncoding));
185	}
186
187	private void processProject(final IReportGroupVisitor visitor,
188			final String bundeName, final MavenProject project,
189			final List<String> includes, final List<String> excludes,
190			final ISourceFileLocator locator) throws IOException {
191		final CoverageBuilder builder = new CoverageBuilder();
192		final File classesDir = new File(project.getBuild()
193				.getOutputDirectory());
194
195		if (classesDir.isDirectory()) {
196			final Analyzer analyzer = new Analyzer(
197					loader.getExecutionDataStore(), builder);
198			final FileFilter filter = new FileFilter(includes, excludes);
199			for (final File file : filter.getFiles(classesDir)) {
200				analyzer.analyzeAll(file);
201			}
202		}
203
204		final IBundleCoverage bundle = builder.getBundle(bundeName);
205		logBundleInfo(bundle, builder.getNoMatchClasses());
206
207		visitor.visitBundle(bundle, locator);
208	}
209
210	private void logBundleInfo(final IBundleCoverage bundle,
211			final Collection<IClassCoverage> nomatch) {
212		log.info(format("Analyzed bundle '%s' with %s classes",
213				bundle.getName(),
214				Integer.valueOf(bundle.getClassCounter().getTotalCount())));
215		if (!nomatch.isEmpty()) {
216			log.warn(format(
217					"Classes in bundle '%s' do no match with execution data. "
218							+ "For report generation the same class files must be used as at runtime.",
219					bundle.getName()));
220			for (final IClassCoverage c : nomatch) {
221				log.warn(format("Execution data for class %s does not match.",
222						c.getName()));
223			}
224		}
225		if (bundle.getClassCounter().getTotalCount() > 0
226				&& bundle.getLineCounter().getTotalCount() == 0) {
227			log.warn("To enable source code annotation class files have to be compiled with debug information.");
228		}
229	}
230
231	private class NoSourceLocator implements ISourceFileLocator {
232
233		public Reader getSourceFile(final String packageName,
234				final String fileName) {
235			return null;
236		}
237
238		public int getTabWidth() {
239			return 0;
240		}
241	}
242
243	private class SourceFileCollection implements ISourceFileLocator {
244
245		private final List<File> sourceRoots;
246		private final String encoding;
247
248		public SourceFileCollection(final MavenProject project,
249				final String encoding) {
250			this.sourceRoots = getCompileSourceRoots(project);
251			this.encoding = encoding;
252		}
253
254		public Reader getSourceFile(final String packageName,
255				final String fileName) throws IOException {
256			final String r;
257			if (packageName.length() > 0) {
258				r = packageName + '/' + fileName;
259			} else {
260				r = fileName;
261			}
262			for (final File sourceRoot : sourceRoots) {
263				final File file = new File(sourceRoot, r);
264				if (file.exists() && file.isFile()) {
265					return new InputStreamReader(new FileInputStream(file),
266							encoding);
267				}
268			}
269			return null;
270		}
271
272		public int getTabWidth() {
273			return 4;
274		}
275	}
276
277	private static List<File> getCompileSourceRoots(final MavenProject project) {
278		final List<File> result = new ArrayList<File>();
279		for (final Object path : project.getCompileSourceRoots()) {
280			result.add(resolvePath(project, (String) path));
281		}
282		return result;
283	}
284
285	private static File resolvePath(final MavenProject project,
286			final String path) {
287		File file = new File(path);
288		if (!file.isAbsolute()) {
289			file = new File(project.getBasedir(), path);
290		}
291		return file;
292	}
293
294}
295