1/*******************************************************************************
2 * Copyright (c) 2009, 2018 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 *    Marc R. Hoffmann - initial API and implementation
10 *
11 *******************************************************************************/
12package org.jacoco.ant;
13
14import static java.lang.String.format;
15
16import java.io.File;
17import java.io.FileOutputStream;
18import java.io.IOException;
19import java.io.InputStream;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Iterator;
23import java.util.List;
24import java.util.Locale;
25import java.util.StringTokenizer;
26
27import org.apache.tools.ant.BuildException;
28import org.apache.tools.ant.Project;
29import org.apache.tools.ant.Task;
30import org.apache.tools.ant.types.Resource;
31import org.apache.tools.ant.types.resources.FileResource;
32import org.apache.tools.ant.types.resources.Union;
33import org.apache.tools.ant.util.FileUtils;
34import org.jacoco.core.analysis.Analyzer;
35import org.jacoco.core.analysis.CoverageBuilder;
36import org.jacoco.core.analysis.IBundleCoverage;
37import org.jacoco.core.analysis.IClassCoverage;
38import org.jacoco.core.analysis.ICoverageNode;
39import org.jacoco.core.data.ExecutionDataStore;
40import org.jacoco.core.data.SessionInfoStore;
41import org.jacoco.core.tools.ExecFileLoader;
42import org.jacoco.report.FileMultiReportOutput;
43import org.jacoco.report.IMultiReportOutput;
44import org.jacoco.report.IReportGroupVisitor;
45import org.jacoco.report.IReportVisitor;
46import org.jacoco.report.MultiReportVisitor;
47import org.jacoco.report.ZipMultiReportOutput;
48import org.jacoco.report.check.IViolationsOutput;
49import org.jacoco.report.check.Limit;
50import org.jacoco.report.check.Rule;
51import org.jacoco.report.check.RulesChecker;
52import org.jacoco.report.csv.CSVFormatter;
53import org.jacoco.report.html.HTMLFormatter;
54import org.jacoco.report.xml.XMLFormatter;
55
56/**
57 * Task for coverage report generation.
58 */
59public class ReportTask extends Task {
60
61	/**
62	 * The source files are specified in a resource collection with additional
63	 * attributes.
64	 */
65	public static class SourceFilesElement extends Union {
66
67		String encoding = null;
68
69		int tabWidth = 4;
70
71		/**
72		 * Defines the optional source file encoding. If not set the platform
73		 * default is used.
74		 *
75		 * @param encoding
76		 *            source file encoding
77		 */
78		public void setEncoding(final String encoding) {
79			this.encoding = encoding;
80		}
81
82		/**
83		 * Sets the tab stop width for the source pages. Default value is 4.
84		 *
85		 * @param tabWidth
86		 *            number of characters per tab stop
87		 */
88		public void setTabwidth(final int tabWidth) {
89			if (tabWidth <= 0) {
90				throw new BuildException("Tab width must be greater than 0");
91			}
92			this.tabWidth = tabWidth;
93		}
94
95	}
96
97	/**
98	 * Container element for class file groups.
99	 */
100	public static class GroupElement {
101
102		private final List<GroupElement> children = new ArrayList<GroupElement>();
103
104		private final Union classfiles = new Union();
105
106		private final SourceFilesElement sourcefiles = new SourceFilesElement();
107
108		private String name;
109
110		/**
111		 * Sets the name of the group.
112		 *
113		 * @param name
114		 *            name of the group
115		 */
116		public void setName(final String name) {
117			this.name = name;
118		}
119
120		/**
121		 * Creates a new child group.
122		 *
123		 * @return new child group
124		 */
125		public GroupElement createGroup() {
126			final GroupElement group = new GroupElement();
127			children.add(group);
128			return group;
129		}
130
131		/**
132		 * Returns the nested resource collection for class files.
133		 *
134		 * @return resource collection for class files
135		 */
136		public Union createClassfiles() {
137			return classfiles;
138		}
139
140		/**
141		 * Returns the nested resource collection for source files.
142		 *
143		 * @return resource collection for source files
144		 */
145		public SourceFilesElement createSourcefiles() {
146			return sourcefiles;
147		}
148
149	}
150
151	/**
152	 * Interface for child elements that define formatters.
153	 */
154	private abstract class FormatterElement {
155
156		abstract IReportVisitor createVisitor() throws IOException;
157
158		void finish() {
159		}
160	}
161
162	/**
163	 * Formatter element for HTML reports.
164	 */
165	public class HTMLFormatterElement extends FormatterElement {
166
167		private File destdir;
168
169		private File destfile;
170
171		private String footer = "";
172
173		private String encoding = "UTF-8";
174
175		private Locale locale = Locale.getDefault();
176
177		/**
178		 * Sets the output directory for the report.
179		 *
180		 * @param destdir
181		 *            output directory
182		 */
183		public void setDestdir(final File destdir) {
184			this.destdir = destdir;
185		}
186
187		/**
188		 * Sets the Zip output file for the report.
189		 *
190		 * @param destfile
191		 *            Zip output file
192		 */
193		public void setDestfile(final File destfile) {
194			this.destfile = destfile;
195		}
196
197		/**
198		 * Sets an optional footer text that will be displayed on every report
199		 * page.
200		 *
201		 * @param text
202		 *            footer text
203		 */
204		public void setFooter(final String text) {
205			this.footer = text;
206		}
207
208		/**
209		 * Sets the output encoding for generated HTML files. Default is UTF-8.
210		 *
211		 * @param encoding
212		 *            output encoding
213		 */
214		public void setEncoding(final String encoding) {
215			this.encoding = encoding;
216		}
217
218		/**
219		 * Sets the locale for generated text output. By default the platform
220		 * locale is used.
221		 *
222		 * @param locale
223		 *            text locale
224		 */
225		public void setLocale(final String locale) {
226			this.locale = parseLocale(locale);
227		}
228
229		@Override
230		public IReportVisitor createVisitor() throws IOException {
231			final IMultiReportOutput output;
232			if (destfile != null) {
233				if (destdir != null) {
234					throw new BuildException(
235							"Either destination directory or file must be supplied, not both",
236							getLocation());
237				}
238				final FileOutputStream stream = new FileOutputStream(destfile);
239				output = new ZipMultiReportOutput(stream);
240
241			} else {
242				if (destdir == null) {
243					throw new BuildException(
244							"Destination directory or file must be supplied for html report",
245							getLocation());
246				}
247				output = new FileMultiReportOutput(destdir);
248			}
249			final HTMLFormatter formatter = new HTMLFormatter();
250			formatter.setFooterText(footer);
251			formatter.setOutputEncoding(encoding);
252			formatter.setLocale(locale);
253			return formatter.createVisitor(output);
254		}
255
256	}
257
258	/**
259	 * Formatter element for CSV reports.
260	 */
261	public class CSVFormatterElement extends FormatterElement {
262
263		private File destfile;
264
265		private String encoding = "UTF-8";
266
267		/**
268		 * Sets the output file for the report.
269		 *
270		 * @param destfile
271		 *            output file
272		 */
273		public void setDestfile(final File destfile) {
274			this.destfile = destfile;
275		}
276
277		@Override
278		public IReportVisitor createVisitor() throws IOException {
279			if (destfile == null) {
280				throw new BuildException(
281						"Destination file must be supplied for csv report",
282						getLocation());
283			}
284			final CSVFormatter formatter = new CSVFormatter();
285			formatter.setOutputEncoding(encoding);
286			return formatter.createVisitor(new FileOutputStream(destfile));
287		}
288
289		/**
290		 * Sets the output encoding for generated XML file. Default is UTF-8.
291		 *
292		 * @param encoding
293		 *            output encoding
294		 */
295		public void setEncoding(final String encoding) {
296			this.encoding = encoding;
297		}
298
299	}
300
301	/**
302	 * Formatter element for XML reports.
303	 */
304	public class XMLFormatterElement extends FormatterElement {
305
306		private File destfile;
307
308		private String encoding = "UTF-8";
309
310		/**
311		 * Sets the output file for the report.
312		 *
313		 * @param destfile
314		 *            output file
315		 */
316		public void setDestfile(final File destfile) {
317			this.destfile = destfile;
318		}
319
320		/**
321		 * Sets the output encoding for generated XML file. Default is UTF-8.
322		 *
323		 * @param encoding
324		 *            output encoding
325		 */
326		public void setEncoding(final String encoding) {
327			this.encoding = encoding;
328		}
329
330		@Override
331		public IReportVisitor createVisitor() throws IOException {
332			if (destfile == null) {
333				throw new BuildException(
334						"Destination file must be supplied for xml report",
335						getLocation());
336			}
337			final XMLFormatter formatter = new XMLFormatter();
338			formatter.setOutputEncoding(encoding);
339			return formatter.createVisitor(new FileOutputStream(destfile));
340		}
341
342	}
343
344	/**
345	 * Formatter element for coverage checks.
346	 */
347	public class CheckFormatterElement extends FormatterElement implements
348			IViolationsOutput {
349
350		private final List<Rule> rules = new ArrayList<Rule>();
351		private boolean violations = false;
352		private boolean failOnViolation = true;
353		private String violationsPropery = null;
354
355		/**
356		 * Creates and adds a new rule.
357		 *
358		 * @return new rule
359		 */
360		public Rule createRule() {
361			final Rule rule = new Rule();
362			rules.add(rule);
363			return rule;
364		}
365
366		/**
367		 * Sets whether the build should fail in case of a violation. Default is
368		 * <code>true</code>.
369		 *
370		 * @param flag
371		 *            if <code>true</code> the build fails on violation
372		 */
373		public void setFailOnViolation(final boolean flag) {
374			this.failOnViolation = flag;
375		}
376
377		/**
378		 * Sets the name of a property to append the violation messages to.
379		 *
380		 * @param property
381		 *            name of a property
382		 */
383		public void setViolationsProperty(final String property) {
384			this.violationsPropery = property;
385		}
386
387		@Override
388		public IReportVisitor createVisitor() throws IOException {
389			final RulesChecker formatter = new RulesChecker();
390			formatter.setRules(rules);
391			return formatter.createVisitor(this);
392		}
393
394		public void onViolation(final ICoverageNode node, final Rule rule,
395				final Limit limit, final String message) {
396			log(message, Project.MSG_ERR);
397			violations = true;
398			if (violationsPropery != null) {
399				final String old = getProject().getProperty(violationsPropery);
400				final String value = old == null ? message : String.format(
401						"%s\n%s", old, message);
402				getProject().setProperty(violationsPropery, value);
403			}
404		}
405
406		@Override
407		void finish() {
408			if (violations && failOnViolation) {
409				throw new BuildException(
410						"Coverage check failed due to violated rules.",
411						getLocation());
412			}
413		}
414	}
415
416	private final Union executiondataElement = new Union();
417
418	private SessionInfoStore sessionInfoStore;
419
420	private ExecutionDataStore executionDataStore;
421
422	private final GroupElement structure = new GroupElement();
423
424	private final List<FormatterElement> formatters = new ArrayList<FormatterElement>();
425
426	/**
427	 * Returns the nested resource collection for execution data files.
428	 *
429	 * @return resource collection for execution files
430	 */
431	public Union createExecutiondata() {
432		return executiondataElement;
433	}
434
435	/**
436	 * Returns the root group element that defines the report structure.
437	 *
438	 * @return root group element
439	 */
440	public GroupElement createStructure() {
441		return structure;
442	}
443
444	/**
445	 * Creates a new HTML report element.
446	 *
447	 * @return HTML report element
448	 */
449	public HTMLFormatterElement createHtml() {
450		final HTMLFormatterElement element = new HTMLFormatterElement();
451		formatters.add(element);
452		return element;
453	}
454
455	/**
456	 * Creates a new CSV report element.
457	 *
458	 * @return CSV report element
459	 */
460	public CSVFormatterElement createCsv() {
461		final CSVFormatterElement element = new CSVFormatterElement();
462		formatters.add(element);
463		return element;
464	}
465
466	/**
467	 * Creates a new coverage check element.
468	 *
469	 * @return coverage check element
470	 */
471	public CheckFormatterElement createCheck() {
472		final CheckFormatterElement element = new CheckFormatterElement();
473		formatters.add(element);
474		return element;
475	}
476
477	/**
478	 * Creates a new XML report element.
479	 *
480	 * @return CSV report element
481	 */
482	public XMLFormatterElement createXml() {
483		final XMLFormatterElement element = new XMLFormatterElement();
484		formatters.add(element);
485		return element;
486	}
487
488	@Override
489	public void execute() throws BuildException {
490		loadExecutionData();
491		try {
492			final IReportVisitor visitor = createVisitor();
493			visitor.visitInfo(sessionInfoStore.getInfos(),
494					executionDataStore.getContents());
495			createReport(visitor, structure);
496			visitor.visitEnd();
497			for (final FormatterElement f : formatters) {
498				f.finish();
499			}
500		} catch (final IOException e) {
501			throw new BuildException("Error while creating report", e,
502					getLocation());
503		}
504	}
505
506	private void loadExecutionData() {
507		final ExecFileLoader loader = new ExecFileLoader();
508		for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
509			final Resource resource = (Resource) i.next();
510			log(format("Loading execution data file %s", resource));
511			InputStream in = null;
512			try {
513				in = resource.getInputStream();
514				loader.load(in);
515			} catch (final IOException e) {
516				throw new BuildException(format(
517						"Unable to read execution data file %s", resource), e,
518						getLocation());
519			} finally {
520				FileUtils.close(in);
521			}
522		}
523		sessionInfoStore = loader.getSessionInfoStore();
524		executionDataStore = loader.getExecutionDataStore();
525	}
526
527	private IReportVisitor createVisitor() throws IOException {
528		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();
529		for (final FormatterElement f : formatters) {
530			visitors.add(f.createVisitor());
531		}
532		return new MultiReportVisitor(visitors);
533	}
534
535	private void createReport(final IReportGroupVisitor visitor,
536			final GroupElement group) throws IOException {
537		if (group.name == null) {
538			throw new BuildException("Group name must be supplied",
539					getLocation());
540		}
541		if (group.children.isEmpty()) {
542			final IBundleCoverage bundle = createBundle(group);
543			final SourceFilesElement sourcefiles = group.sourcefiles;
544			final AntResourcesLocator locator = new AntResourcesLocator(
545					sourcefiles.encoding, sourcefiles.tabWidth);
546			locator.addAll(sourcefiles.iterator());
547			if (!locator.isEmpty()) {
548				checkForMissingDebugInformation(bundle);
549			}
550			visitor.visitBundle(bundle, locator);
551		} else {
552			final IReportGroupVisitor groupVisitor = visitor
553					.visitGroup(group.name);
554			for (final GroupElement child : group.children) {
555				createReport(groupVisitor, child);
556			}
557		}
558	}
559
560	private IBundleCoverage createBundle(final GroupElement group)
561			throws IOException {
562		final CoverageBuilder builder = new CoverageBuilder();
563		final Analyzer analyzer = new Analyzer(executionDataStore, builder);
564		for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
565			final Resource resource = (Resource) i.next();
566			if (resource.isDirectory() && resource instanceof FileResource) {
567				analyzer.analyzeAll(((FileResource) resource).getFile());
568			} else {
569				final InputStream in = resource.getInputStream();
570				analyzer.analyzeAll(in, resource.getName());
571				in.close();
572			}
573		}
574		final IBundleCoverage bundle = builder.getBundle(group.name);
575		logBundleInfo(bundle, builder.getNoMatchClasses());
576		return bundle;
577	}
578
579	private void logBundleInfo(final IBundleCoverage bundle,
580			final Collection<IClassCoverage> nomatch) {
581		log(format("Writing bundle '%s' with %s classes", bundle.getName(),
582				Integer.valueOf(bundle.getClassCounter().getTotalCount())));
583		if (!nomatch.isEmpty()) {
584			log(format(
585					"Classes in bundle '%s' do no match with execution data. "
586							+ "For report generation the same class files must be used as at runtime.",
587					bundle.getName()), Project.MSG_WARN);
588			for (final IClassCoverage c : nomatch) {
589				log(format("Execution data for class %s does not match.",
590						c.getName()), Project.MSG_WARN);
591			}
592		}
593	}
594
595	private void checkForMissingDebugInformation(final ICoverageNode node) {
596		if (node.getClassCounter().getTotalCount() > 0
597				&& node.getLineCounter().getTotalCount() == 0) {
598			log(format(
599					"To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",
600					node.getName()), Project.MSG_WARN);
601		}
602	}
603
604	/**
605	 * Splits a given underscore "_" separated string and creates a Locale. This
606	 * method is implemented as the method Locale.forLanguageTag() was not
607	 * available in Java 5.
608	 *
609	 * @param locale
610	 *            String representation of a Locate
611	 * @return Locale instance
612	 */
613	static Locale parseLocale(final String locale) {
614		final StringTokenizer st = new StringTokenizer(locale, "_");
615		final String language = st.hasMoreTokens() ? st.nextToken() : "";
616		final String country = st.hasMoreTokens() ? st.nextToken() : "";
617		final String variant = st.hasMoreTokens() ? st.nextToken() : "";
618		return new Locale(language, country, variant);
619	}
620
621}
622