content: /*******************************************************************************
   * Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   *
   * Contributors:
   *    Marc R. Hoffmann - initial API and implementation
   *    
   *******************************************************************************/
  package org.jacoco.ant;
  
  import static java.lang.String.format;
  
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Locale;
  import java.util.StringTokenizer;
  
  import org.apache.tools.ant.BuildException;
  import org.apache.tools.ant.Project;
  import org.apache.tools.ant.Task;
  import org.apache.tools.ant.types.Resource;
  import org.apache.tools.ant.types.resources.FileResource;
  import org.apache.tools.ant.types.resources.Union;
  import org.apache.tools.ant.util.FileUtils;
  import org.jacoco.core.analysis.Analyzer;
  import org.jacoco.core.analysis.CoverageBuilder;
  import org.jacoco.core.analysis.IBundleCoverage;
  import org.jacoco.core.analysis.IClassCoverage;
  import org.jacoco.core.analysis.ICoverageNode;
  import org.jacoco.core.data.ExecutionDataStore;
  import org.jacoco.core.data.SessionInfoStore;
  import org.jacoco.core.tools.ExecFileLoader;
  import org.jacoco.report.FileMultiReportOutput;
  import org.jacoco.report.IMultiReportOutput;
  import org.jacoco.report.IReportGroupVisitor;
  import org.jacoco.report.IReportVisitor;
  import org.jacoco.report.MultiReportVisitor;
  import org.jacoco.report.ZipMultiReportOutput;
  import org.jacoco.report.check.IViolationsOutput;
  import org.jacoco.report.check.Limit;
  import org.jacoco.report.check.Rule;
  import org.jacoco.report.check.RulesChecker;
  import org.jacoco.report.csv.CSVFormatter;
  import org.jacoco.report.html.HTMLFormatter;
  import org.jacoco.report.xml.XMLFormatter;
  
  /**
   * Task for coverage report generation.
   */
  public class ReportTask extends Task {
  
  	/**
  	 * The source files are specified in a resource collection with additional
  	 * attributes.
  	 */
  	public static class SourceFilesElement extends Union {
  
  		String encoding = null;
  
  		int tabWidth = 4;
  
  		/**
  		 * Defines the optional source file encoding. If not set the platform
  		 * default is used.
  		 * 
  		 * @param encoding
  		 *            source file encoding
  		 */
  		public void setEncoding(final String encoding) {
  			this.encoding = encoding;
  		}
  
  		/**
  		 * Sets the tab stop width for the source pages. Default value is 4.
  		 * 
  		 * @param tabWidth
  		 *            number of characters per tab stop
  		 */
  		public void setTabwidth(final int tabWidth) {
  			if (tabWidth <= 0) {
  				throw new BuildException("Tab width must be greater than 0");
  			}
  			this.tabWidth = tabWidth;
  		}
  
  	}
  
  	/**
  	 * Container element for class file groups.
  	 */
  	public static class GroupElement {
  
  		private final List<GroupElement> children = new ArrayList<GroupElement>();
  
  		private final Union classfiles = new Union();
  
  		private final SourceFilesElement sourcefiles = new SourceFilesElement();
  
  		private String name;
  
  		/**
  		 * Sets the name of the group.
  		 * 
  		 * @param name
  		 *            name of the group
  		 */
  		public void setName(final String name) {
  			this.name = name;
  		}
  
  		/**
  		 * Creates a new child group.
  		 * 
  		 * @return new child group
  		 */
  		public GroupElement createGroup() {
  			final GroupElement group = new GroupElement();
  			children.add(group);
  			return group;
  		}
  
  		/**
  		 * Returns the nested resource collection for class files.
  		 * 
  		 * @return resource collection for class files
  		 */
  		public Union createClassfiles() {
  			return classfiles;
  		}
  
  		/**
  		 * Returns the nested resource collection for source files.
  		 * 
  		 * @return resource collection for source files
  		 */
  		public SourceFilesElement createSourcefiles() {
  			return sourcefiles;
  		}
  
  	}
  
  	/**
  	 * Interface for child elements that define formatters.
  	 */
  	private abstract class FormatterElement {
  
  		abstract IReportVisitor createVisitor() throws IOException;
  
  		void finish() {
  		}
  	}
  
  	/**
  	 * Formatter element for HTML reports.
  	 */
  	public class HTMLFormatterElement extends FormatterElement {
  
  		private File destdir;
  
  		private File destfile;
  
  		private String footer = "";
  
  		private String encoding = "UTF-8";
  
  		private Locale locale = Locale.getDefault();
  
  		/**
  		 * Sets the output directory for the report.
  		 * 
  		 * @param destdir
  		 *            output directory
  		 */
  		public void setDestdir(final File destdir) {
  			this.destdir = destdir;
  		}
  
  		/**
  		 * Sets the Zip output file for the report.
  		 * 
  		 * @param destfile
  		 *            Zip output file
  		 */
  		public void setDestfile(final File destfile) {
  			this.destfile = destfile;
  		}
  
  		/**
  		 * Sets an optional footer text that will be displayed on every report
  		 * page.
  		 * 
  		 * @param text
  		 *            footer text
  		 */
  		public void setFooter(final String text) {
  			this.footer = text;
  		}
  
  		/**
  		 * Sets the output encoding for generated HTML files. Default is UTF-8.
  		 * 
  		 * @param encoding
  		 *            output encoding
  		 */
  		public void setEncoding(final String encoding) {
  			this.encoding = encoding;
  		}
  
  		/**
  		 * Sets the locale for generated text output. By default the platform
  		 * locale is used.
  		 * 
  		 * @param locale
  		 *            text locale
  		 */
  		public void setLocale(final String locale) {
  			this.locale = parseLocale(locale);
  		}
  
  		@Override
  		public IReportVisitor createVisitor() throws IOException {
  			final IMultiReportOutput output;
  			if (destfile != null) {
  				if (destdir != null) {
  					throw new BuildException(
  							"Either destination directory or file must be supplied, not both",
  							getLocation());
  				}
  				final FileOutputStream stream = new FileOutputStream(destfile);
  				output = new ZipMultiReportOutput(stream);
  
  			} else {
  				if (destdir == null) {
  					throw new BuildException(
  							"Destination directory or file must be supplied for html report",
  							getLocation());
  				}
  				output = new FileMultiReportOutput(destdir);
  			}
  			final HTMLFormatter formatter = new HTMLFormatter();
  			formatter.setFooterText(footer);
  			formatter.setOutputEncoding(encoding);
  			formatter.setLocale(locale);
  			return formatter.createVisitor(output);
  		}
  
  	}
  
  	/**
  	 * Formatter element for CSV reports.
  	 */
  	public class CSVFormatterElement extends FormatterElement {
  
  		private File destfile;
  
  		private String encoding = "UTF-8";
  
  		/**
  		 * Sets the output file for the report.
  		 * 
  		 * @param destfile
  		 *            output file
  		 */
  		public void setDestfile(final File destfile) {
  			this.destfile = destfile;
  		}
  
  		@Override
  		public IReportVisitor createVisitor() throws IOException {
  			if (destfile == null) {
  				throw new BuildException(
  						"Destination file must be supplied for csv report",
  						getLocation());
  			}
  			final CSVFormatter formatter = new CSVFormatter();
  			formatter.setOutputEncoding(encoding);
  			return formatter.createVisitor(new FileOutputStream(destfile));
  		}
  
  		/**
  		 * Sets the output encoding for generated XML file. Default is UTF-8.
  		 * 
  		 * @param encoding
  		 *            output encoding
  		 */
  		public void setEncoding(final String encoding) {
  			this.encoding = encoding;
  		}
  
  	}
  
  	/**
  	 * Formatter element for XML reports.
  	 */
  	public class XMLFormatterElement extends FormatterElement {
  
  		private File destfile;
  
  		private String encoding = "UTF-8";
  
  		/**
  		 * Sets the output file for the report.
  		 * 
  		 * @param destfile
  		 *            output file
  		 */
  		public void setDestfile(final File destfile) {
  			this.destfile = destfile;
  		}
  
  		/**
  		 * Sets the output encoding for generated XML file. Default is UTF-8.
  		 * 
  		 * @param encoding
  		 *            output encoding
  		 */
  		public void setEncoding(final String encoding) {
  			this.encoding = encoding;
  		}
  
  		@Override
  		public IReportVisitor createVisitor() throws IOException {
  			if (destfile == null) {
  				throw new BuildException(
  						"Destination file must be supplied for xml report",
  						getLocation());
  			}
  			final XMLFormatter formatter = new XMLFormatter();
  			formatter.setOutputEncoding(encoding);
  			return formatter.createVisitor(new FileOutputStream(destfile));
  		}
  
  	}
  
  	/**
  	 * Formatter element for coverage checks.
  	 */
  	public class CheckFormatterElement extends FormatterElement implements
  			IViolationsOutput {
  
  		private final List<Rule> rules = new ArrayList<Rule>();
  		private boolean violations = false;
  		private boolean failOnViolation = true;
  		private String violationsPropery = null;
  
  		/**
  		 * Creates and adds a new rule.
  		 * 
  		 * @return new rule
  		 */
  		public Rule createRule() {
  			final Rule rule = new Rule();
  			rules.add(rule);
  			return rule;
  		}
  
  		/**
  		 * Sets whether the build should fail in case of a violation. Default is
  		 * <code>true</code>.
  		 * 
  		 * @param flag
  		 *            if <code>true</code> the build fails on violation
  		 */
  		public void setFailOnViolation(final boolean flag) {
  			this.failOnViolation = flag;
  		}
  
  		/**
  		 * Sets the name of a property to append the violation messages to.
  		 * 
  		 * @param property
  		 *            name of a property
  		 */
  		public void setViolationsProperty(final String property) {
  			this.violationsPropery = property;
  		}
  
  		@Override
  		public IReportVisitor createVisitor() throws IOException {
  			final RulesChecker formatter = new RulesChecker();
  			formatter.setRules(rules);
  			return formatter.createVisitor(this);
  		}
  
  		public void onViolation(final ICoverageNode node, final Rule rule,
  				final Limit limit, final String message) {
  			log(message, Project.MSG_ERR);
  			violations = true;
  			if (violationsPropery != null) {
  				final String old = getProject().getProperty(violationsPropery);
  				final String value = old == null ? message : String.format(
  						"%s\n%s", old, message);
  				getProject().setProperty(violationsPropery, value);
  			}
  		}
  
  		@Override
  		void finish() {
  			if (violations && failOnViolation) {
  				throw new BuildException(
  						"Coverage check failed due to violated rules.",
  						getLocation());
  			}
  		}
  	}
  
  	private final Union executiondataElement = new Union();
  
  	private SessionInfoStore sessionInfoStore;
  
  	private ExecutionDataStore executionDataStore;
  
  	private final GroupElement structure = new GroupElement();
  
  	private final List<FormatterElement> formatters = new ArrayList<FormatterElement>();
  
  	/**
  	 * Returns the nested resource collection for execution data files.
  	 * 
  	 * @return resource collection for execution files
  	 */
  	public Union createExecutiondata() {
  		return executiondataElement;
  	}
  
  	/**
  	 * Returns the root group element that defines the report structure.
  	 * 
  	 * @return root group element
  	 */
  	public GroupElement createStructure() {
  		return structure;
  	}
  
  	/**
  	 * Creates a new HTML report element.
  	 * 
  	 * @return HTML report element
  	 */
  	public HTMLFormatterElement createHtml() {
  		final HTMLFormatterElement element = new HTMLFormatterElement();
  		formatters.add(element);
  		return element;
  	}
  
  	/**
  	 * Creates a new CSV report element.
  	 * 
  	 * @return CSV report element
  	 */
  	public CSVFormatterElement createCsv() {
  		final CSVFormatterElement element = new CSVFormatterElement();
  		formatters.add(element);
  		return element;
  	}
  
  	/**
  	 * Creates a new coverage check element.
  	 * 
  	 * @return coverage check element
  	 */
  	public CheckFormatterElement createCheck() {
  		final CheckFormatterElement element = new CheckFormatterElement();
  		formatters.add(element);
  		return element;
  	}
  
  	/**
  	 * Creates a new XML report element.
  	 * 
  	 * @return CSV report element
  	 */
  	public XMLFormatterElement createXml() {
  		final XMLFormatterElement element = new XMLFormatterElement();
  		formatters.add(element);
  		return element;
  	}
  
  	@Override
  	public void execute() throws BuildException {
  		loadExecutionData();
  		try {
  			final IReportVisitor visitor = createVisitor();
  			visitor.visitInfo(sessionInfoStore.getInfos(),
  					executionDataStore.getContents());
  			createReport(visitor, structure);
  			visitor.visitEnd();
  			for (final FormatterElement f : formatters) {
  				f.finish();
  			}
  		} catch (final IOException e) {
  			throw new BuildException("Error while creating report", e,
  					getLocation());
  		}
  	}
  
  	private void loadExecutionData() {
  		final ExecFileLoader loader = new ExecFileLoader();
  		for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
  			final Resource resource = (Resource) i.next();
  			log(format("Loading execution data file %s", resource));
  			InputStream in = null;
  			try {
  				in = resource.getInputStream();
  				loader.load(in);
  			} catch (final IOException e) {
  				throw new BuildException(format(
  						"Unable to read execution data file %s", resource), e,
  						getLocation());
  			} finally {
  				FileUtils.close(in);
  			}
  		}
  		sessionInfoStore = loader.getSessionInfoStore();
  		executionDataStore = loader.getExecutionDataStore();
  	}
  
  	private IReportVisitor createVisitor() throws IOException {
  		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();
  		for (final FormatterElement f : formatters) {
  			visitors.add(f.createVisitor());
  		}
  		return new MultiReportVisitor(visitors);
  	}
  
  	private void createReport(final IReportGroupVisitor visitor,
  			final GroupElement group) throws IOException {
  		if (group.name == null) {
  			throw new BuildException("Group name must be supplied",
  					getLocation());
  		}
  		if (group.children.isEmpty()) {
  			final IBundleCoverage bundle = createBundle(group);
  			final SourceFilesElement sourcefiles = group.sourcefiles;
  			final AntResourcesLocator locator = new AntResourcesLocator(
  					sourcefiles.encoding, sourcefiles.tabWidth);
  			locator.addAll(sourcefiles.iterator());
  			if (!locator.isEmpty()) {
  				checkForMissingDebugInformation(bundle);
  			}
  			visitor.visitBundle(bundle, locator);
  		} else {
  			final IReportGroupVisitor groupVisitor = visitor
  					.visitGroup(group.name);
  			for (final GroupElement child : group.children) {
  				createReport(groupVisitor, child);
  			}
  		}
  	}
  
  	private IBundleCoverage createBundle(final GroupElement group)
  			throws IOException {
  		final CoverageBuilder builder = new CoverageBuilder();
  		final Analyzer analyzer = new Analyzer(executionDataStore, builder);
  		for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
  			final Resource resource = (Resource) i.next();
  			if (resource.isDirectory() && resource instanceof FileResource) {
  				analyzer.analyzeAll(((FileResource) resource).getFile());
  			} else {
  				final InputStream in = resource.getInputStream();
  				analyzer.analyzeAll(in, resource.getName());
  				in.close();
  			}
  		}
  		final IBundleCoverage bundle = builder.getBundle(group.name);
  		logBundleInfo(bundle, builder.getNoMatchClasses());
  		return bundle;
  	}
  
  	private void logBundleInfo(final IBundleCoverage bundle,
  			final Collection<IClassCoverage> nomatch) {
  		log(format("Writing bundle '%s' with %s classes", bundle.getName(),
  				Integer.valueOf(bundle.getClassCounter().getTotalCount())));
  		if (!nomatch.isEmpty()) {
  			log(format(
  					"Classes in bundle '%s' do no match with execution data. "
  							+ "For report generation the same class files must be used as at runtime.",
  					bundle.getName()), Project.MSG_WARN);
  			for (final IClassCoverage c : nomatch) {
  				log(format("Execution data for class %s does not match.",
  						c.getName()), Project.MSG_WARN);
  			}
  		}
  	}
  
  	private void checkForMissingDebugInformation(final ICoverageNode node) {
  		if (node.containsCode() && node.getLineCounter().getTotalCount() == 0) {
  			log(format(
  					"To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",
  					node.getName()), Project.MSG_WARN);
  		}
  	}
  
  	/**
  	 * Splits a given underscore "_" separated string and creates a Locale. This
  	 * method is implemented as the method Locale.forLanguageTag() was not
  	 * available in Java 5.
  	 * 
  	 * @param locale
  	 *            String representation of a Locate
  	 * @return Locale instance
  	 */
  	static Locale parseLocale(final String locale) {
  		final StringTokenizer st = new StringTokenizer(locale, "_");
  		final String language = st.hasMoreTokens() ? st.nextToken() : "";
  		final String country = st.hasMoreTokens() ? st.nextToken() : "";
  		final String variant = st.hasMoreTokens() ? st.nextToken() : "";
  		return new Locale(language, country, variant);
  	}
  
  }
  