ReportTask.java

    1/*******************************************************************************
    2 * Copyright (c) 2009, 2010 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 * $Id: $
   12 *******************************************************************************/
   13package org.jacoco.ant;
   14
   15import java.io.BufferedInputStream;
   16import java.io.File;
   17import java.io.FileOutputStream;
   18import java.io.IOException;
   19import java.io.InputStream;
   20import java.io.InputStreamReader;
   21import java.io.Reader;
   22import java.util.ArrayList;
   23import java.util.Collection;
   24import java.util.HashMap;
   25import java.util.Iterator;
   26import java.util.List;
   27import java.util.Map;
   28import java.util.zip.ZipOutputStream;
   29
   30import org.apache.tools.ant.BuildException;
   31import org.apache.tools.ant.Task;
   32import org.apache.tools.ant.types.Resource;
   33import org.apache.tools.ant.types.resources.FileResource;
   34import org.apache.tools.ant.types.resources.Union;
   35import org.apache.tools.ant.util.FileUtils;
   36import org.jacoco.core.analysis.BundleCoverage;
   37import org.jacoco.core.analysis.ClassCoverage;
   38import org.jacoco.core.analysis.CoverageBuilder;
   39import org.jacoco.core.analysis.CoverageNodeImpl;
   40import org.jacoco.core.analysis.ICoverageNode;
   41import org.jacoco.core.analysis.PackageCoverage;
   42import org.jacoco.core.analysis.ICoverageNode.ElementType;
   43import org.jacoco.core.data.ExecutionDataReader;
   44import org.jacoco.core.data.ExecutionDataStore;
   45import org.jacoco.core.instr.Analyzer;
   46import org.jacoco.report.FileMultiReportOutput;
   47import org.jacoco.report.FileSingleReportOutput;
   48import org.jacoco.report.IMultiReportOutput;
   49import org.jacoco.report.IReportFormatter;
   50import org.jacoco.report.IReportVisitor;
   51import org.jacoco.report.ISourceFileLocator;
   52import org.jacoco.report.MultiFormatter;
   53import org.jacoco.report.ZipMultiReportOutput;
   54import org.jacoco.report.csv.CsvFormatter;
   55import org.jacoco.report.html.HTMLFormatter;
   56import org.jacoco.report.xml.XMLFormatter;
   57
   58/**
   59 * Task for coverage report generation. Experimental implementation that needs
   60 * refinement.
   61 * 
   62 * @author Marc R. Hoffmann
   63 * @version $Revision: $
   64 */
   65public class ReportTask extends Task {
   66
   67    /**
   68     * The source files are specified in a resource collection with additional
   69     * attributes.
   70     */
   71    public static class SourceFilesElement extends Union {
   72
   73        String encoding;
   74
   75        /**
   76         * Defines the optional source file encoding. If not set the platform
   77         * default is used.
   78         * 
   79         * @param encoding
   80         *            source file encoding
   81         */
   82        public void setEncoding(final String encoding) {
   83            this.encoding = encoding;
   84        }
   85
   86    }
   87
   88    /**
   89     * Container element for class file groups.
   90     */
   91    public static class GroupElement {
   92
   93        private final List<GroupElement> children = new ArrayList<GroupElement>();
   94
   95        private final Union classfiles = new Union();
   96
   97        private final SourceFilesElement sourcefiles = new SourceFilesElement();
   98
   99        private String name;
  100
  101        /**
  102         * Sets the name of the group.
  103         * 
  104         * @param name
  105         *            name of the group
  106         */
  107        public void setName(final String name) {
  108            this.name = name;
  109        }
  110
  111        /**
  112         * Creates a new child group.
  113         * 
  114         * @return new child group
  115         */
  116        public GroupElement createGroup() {
  117            final GroupElement group = new GroupElement();
  118            children.add(group);
  119            return group;
  120        }
  121
  122        /**
  123         * Returns the nested resource collection for class files.
  124         * 
  125         * @return resource collection for class files
  126         */
  127        public Union createClassfiles() {
  128            return classfiles;
  129        }
  130
  131        /**
  132         * Returns the nested resource collection for source files.
  133         * 
  134         * @return resource collection for source files
  135         */
  136        public SourceFilesElement createSourcefiles() {
  137            return sourcefiles;
  138        }
  139
  140    }
  141
  142    /**
  143     * Interface for child elements that define formatters.
  144     */
  145    private interface IFormatterElement {
  146
  147        IReportFormatter createFormatter() throws IOException;
  148
  149        void finish() throws IOException;
  150
  151    }
  152
  153    /**
  154     * Formatter Element for HTML reports.
  155     */
  156    public static class HTMLFormatterElement implements IFormatterElement {
  157
  158        private File destdir;
  159
  160        private File destfile;
  161
  162        private String footer = "";
  163
  164        private String encoding = "UTF-8";
  165
  166        private ZipOutputStream zipOutput;
  167
  168        /**
  169         * Sets the output directory for the report.
  170         * 
  171         * @param destdir
  172         *            output directory
  173         */
  174        public void setDestdir(final File destdir) {
  175            this.destdir = destdir;
  176        }
  177
  178        /**
  179         * Sets the Zip output file for the report.
  180         * 
  181         * @param destfile
  182         *            Zip output file
  183         */
  184        public void setDestfile(final File destfile) {
  185            this.destfile = destfile;
  186        }
  187
  188        /**
  189         * Sets an optional footer text that will be displayed on every report
  190         * page.
  191         * 
  192         * @param text
  193         *            footer text
  194         */
  195        public void setFooter(final String text) {
  196            this.footer = text;
  197        }
  198
  199        /**
  200         * Sets the output encoding for generated HTML files. Default is UTF-8.
  201         * 
  202         * @param encoding
  203         *            output encoding
  204         */
  205        public void setEncoding(final String encoding) {
  206            this.encoding = encoding;
  207        }
  208
  209        public IReportFormatter createFormatter() throws IOException {
  210            final IMultiReportOutput output;
  211            if (destfile != null) {
  212                if (destdir != null) {
  213                    throw new BuildException(
  214                            "Either destination directory or file must be supplied, not both");
  215                }
  216                zipOutput = new ZipOutputStream(new FileOutputStream(destfile));
  217                output = new ZipMultiReportOutput(zipOutput);
  218
  219            } else {
  220                if (destdir == null) {
  221                    throw new BuildException(
  222                            "Destination directory or file must be supplied for html report");
  223                }
  224                output = new FileMultiReportOutput(destdir);
  225            }
  226            final HTMLFormatter formatter = new HTMLFormatter();
  227            formatter.setReportOutput(output);
  228            formatter.setFooterText(footer);
  229            formatter.setOutputEncoding(encoding);
  230            return formatter;
  231        }
  232
  233        public void finish() throws IOException {
  234            if (zipOutput != null) {
  235                zipOutput.close();
  236            }
  237        }
  238
  239    }
  240
  241    /**
  242     * Formatter Element for CSV reports.
  243     */
  244    public static class CsvFormatterElement implements IFormatterElement {
  245
  246        private File destfile;
  247
  248        private String encoding = "UTF-8";
  249
  250        /**
  251         * Sets the output file for the report.
  252         * 
  253         * @param destfile
  254         *            output file
  255         */
  256        public void setDestfile(final File destfile) {
  257            this.destfile = destfile;
  258        }
  259
  260        public IReportFormatter createFormatter() {
  261            if (destfile == null) {
  262                throw new BuildException(
  263                        "Destination file must be supplied for csv report");
  264            }
  265            final CsvFormatter formatter = new CsvFormatter();
  266            formatter.setReportOutput(new FileSingleReportOutput(destfile));
  267            formatter.setOutputEncoding(encoding);
  268            return formatter;
  269        }
  270
  271        /**
  272         * Sets the output encoding for generated XML file. Default is UTF-8.
  273         * 
  274         * @param encoding
  275         *            output encoding
  276         */
  277        public void setEncoding(final String encoding) {
  278            this.encoding = encoding;
  279        }
  280
  281        public void finish() {
  282        }
  283
  284    }
  285
  286    /**
  287     * Formatter Element for XML reports.
  288     */
  289    public static class XMLFormatterElement implements IFormatterElement {
  290
  291        private File destfile;
  292
  293        private String encoding = "UTF-8";
  294
  295        /**
  296         * Sets the output file for the report.
  297         * 
  298         * @param destfile
  299         *            output file
  300         */
  301        public void setDestfile(final File destfile) {
  302            this.destfile = destfile;
  303        }
  304
  305        /**
  306         * Sets the output encoding for generated XML file. Default is UTF-8.
  307         * 
  308         * @param encoding
  309         *            output encoding
  310         */
  311        public void setEncoding(final String encoding) {
  312            this.encoding = encoding;
  313        }
  314
  315        public IReportFormatter createFormatter() {
  316            if (destfile == null) {
  317                throw new BuildException(
  318                        "Destination file must be supplied for xml report");
  319            }
  320            final XMLFormatter formatter = new XMLFormatter();
  321            formatter.setReportOutput(new FileSingleReportOutput(destfile));
  322            formatter.setOutputEncoding(encoding);
  323            return formatter;
  324        }
  325
  326        public void finish() {
  327        }
  328
  329    }
  330
  331    private final Union executiondataElement = new Union();
  332
  333    private final GroupElement structure = new GroupElement();
  334
  335    private final List<IFormatterElement> formatters = new ArrayList<IFormatterElement>();
  336
  337    /**
  338     * Returns the nested resource collection for execution data files.
  339     * 
  340     * @return resource collection for execution files
  341     */
  342    public Union createExecutiondata() {
  343        return executiondataElement;
  344    }
  345
  346    /**
  347     * Returns the root group element that defines the report structure.
  348     * 
  349     * @return root group element
  350     */
  351    public GroupElement createStructure() {
  352        return structure;
  353    }
  354
  355    /**
  356     * Creates a new HTML report element.
  357     * 
  358     * @return HTML report element
  359     */
  360    public HTMLFormatterElement createHtml() {
  361        final HTMLFormatterElement element = new HTMLFormatterElement();
  362        formatters.add(element);
  363        return element;
  364    }
  365
  366    /**
  367     * Creates a new CSV report element.
  368     * 
  369     * @return CSV report element
  370     */
  371    public CsvFormatterElement createCsv() {
  372        final CsvFormatterElement element = new CsvFormatterElement();
  373        formatters.add(element);
  374        return element;
  375    }
  376
  377    /**
  378     * Creates a new XML report element.
  379     * 
  380     * @return CSV report element
  381     */
  382    public XMLFormatterElement createXml() {
  383        final XMLFormatterElement element = new XMLFormatterElement();
  384        formatters.add(element);
  385        return element;
  386    }
  387
  388    @Override
  389    public void execute() throws BuildException {
  390        final ExecutionDataStore executionData = loadExecutionData();
  391        try {
  392            final IReportFormatter formatter = createFormatter();
  393            createReport(structure, formatter, executionData);
  394            finishFormatters();
  395        } catch (final IOException e) {
  396            throw new BuildException("Error while creating report.", e);
  397        }
  398    }
  399
  400    private ExecutionDataStore loadExecutionData() {
  401        final ExecutionDataStore data = new ExecutionDataStore();
  402        for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
  403            final Resource resource = (Resource) i.next();
  404            InputStream in = null;
  405            try {
  406                in = new BufferedInputStream(resource.getInputStream());
  407                final ExecutionDataReader reader = new ExecutionDataReader(in);
  408                reader.setExecutionDataVisitor(data);
  409                reader.read();
  410            } catch (final IOException e) {
  411                throw new BuildException("Unable to read execution data file "
  412                        + resource.getName(), e);
  413            } finally {
  414                FileUtils.close(in);
  415            }
  416        }
  417        return data;
  418    }
  419
  420    private IReportFormatter createFormatter() throws IOException {
  421        final MultiFormatter multi = new MultiFormatter();
  422        for (final IFormatterElement f : formatters) {
  423            multi.add(f.createFormatter());
  424        }
  425        return multi;
  426    }
  427
  428    private void finishFormatters() throws IOException {
  429        for (final IFormatterElement f : formatters) {
  430            f.finish();
  431        }
  432    }
  433
  434    private void createReport(final GroupElement group,
  435            final IReportFormatter formatter,
  436            final ExecutionDataStore executionData) throws IOException {
  437        final CoverageNodeImpl node = createNode(group, executionData);
  438        final IReportVisitor visitor = formatter.createReportVisitor(node);
  439        final SourceFileCollection sourceFileLocator = new SourceFileCollection(
  440                group.sourcefiles);
  441        if (node instanceof BundleCoverage) {
  442            visitBundle(visitor, (BundleCoverage) node, sourceFileLocator);
  443        } else {
  444            for (final GroupElement g : group.children) {
  445                createReport(g, visitor, node, executionData);
  446            }
  447        }
  448        visitor.visitEnd(sourceFileLocator);
  449    }
  450
  451    private void createReport(final GroupElement group,
  452            final IReportVisitor parentVisitor,
  453            final CoverageNodeImpl parentNode,
  454            final ExecutionDataStore executionData) throws IOException {
  455        final CoverageNodeImpl node = createNode(group, executionData);
  456        final IReportVisitor visitor = parentVisitor.visitChild(node);
  457        final SourceFileCollection sourceFileLocator = new SourceFileCollection(
  458                group.sourcefiles);
  459        if (node instanceof BundleCoverage) {
  460            visitBundle(visitor, (BundleCoverage) node, sourceFileLocator);
  461        } else {
  462            for (final GroupElement g : group.children) {
  463                createReport(g, visitor, node, executionData);
  464            }
  465        }
  466        parentNode.increment(node);
  467        visitor.visitEnd(sourceFileLocator);
  468    }
  469
  470    private CoverageNodeImpl createNode(final GroupElement group,
  471            final ExecutionDataStore executionData) throws IOException {
  472        if (group.name == null) {
  473            throw new BuildException("Group name must be supplied");
  474        }
  475        if (group.children.size() > 0) {
  476            return new CoverageNodeImpl(ElementType.GROUP, group.name, false);
  477        } else {
  478            final CoverageBuilder builder = new CoverageBuilder(executionData);
  479            final Analyzer analyzer = new Analyzer(builder);
  480            for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
  481                final Resource resource = (Resource) i.next();
  482                if (resource.isDirectory() && resource instanceof FileResource) {
  483                    analyzer.analyzeAll(((FileResource) resource).getFile());
  484                } else {
  485                    final InputStream in = resource.getInputStream();
  486                    analyzer.analyzeAll(in);
  487                    in.close();
  488                }
  489            }
  490            return builder.getBundle(group.name);
  491        }
  492    }
  493
  494    private static class SourceFileCollection implements ISourceFileLocator {
  495
  496        private final String encoding;
  497
  498        private final Map<String, Resource> resources = new HashMap<String, Resource>();
  499
  500        SourceFileCollection(final SourceFilesElement sourceFiles) {
  501            encoding = sourceFiles.encoding;
  502            for (final Iterator<?> i = sourceFiles.iterator(); i.hasNext();) {
  503                final Resource r = (Resource) i.next();
  504                resources.put(r.getName().replace(File.separatorChar, '/'), r);
  505            }
  506        }
  507
  508        public Reader getSourceFile(final String packageName,
  509                final String fileName) throws IOException {
  510            final Resource r = resources.get(packageName + '/' + fileName);
  511            if (r == null) {
  512                return null;
  513            }
  514            if (encoding == null) {
  515                return new InputStreamReader(r.getInputStream());
  516            } else {
  517                return new InputStreamReader(r.getInputStream(), encoding);
  518            }
  519        }
  520    }
  521
  522    private static void visitBundle(final IReportVisitor visitor,
  523            final BundleCoverage bundledata,
  524            final ISourceFileLocator sourceFileLocator) throws IOException {
  525        for (final PackageCoverage p : bundledata.getPackages()) {
  526            visitPackage(visitor.visitChild(p), p, sourceFileLocator);
  527        }
  528    }
  529
  530    private static void visitPackage(final IReportVisitor visitor,
  531            final PackageCoverage packagedata,
  532            final ISourceFileLocator sourceFileLocator) throws IOException {
  533        visitLeafs(visitor, packagedata.getSourceFiles(), sourceFileLocator);
  534        for (final ClassCoverage c : packagedata.getClasses()) {
  535            visitClass(visitor.visitChild(c), c, sourceFileLocator);
  536        }
  537        visitor.visitEnd(sourceFileLocator);
  538    }
  539
  540    private static void visitClass(final IReportVisitor visitor,
  541            final ClassCoverage classdata,
  542            final ISourceFileLocator sourceFileLocator) throws IOException {
  543        visitLeafs(visitor, classdata.getMethods(), sourceFileLocator);
  544        visitor.visitEnd(sourceFileLocator);
  545    }
  546
  547    private static void visitLeafs(final IReportVisitor visitor,
  548            final Collection<? extends ICoverageNode> leafs,
  549            final ISourceFileLocator sourceFileLocator) throws IOException {
  550        for (final ICoverageNode l : leafs) {
  551            final IReportVisitor child = visitor.visitChild(l);
  552            child.visitEnd(sourceFileLocator);
  553        }
  554    }
  555
  556}