You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@creadur.apache.org by cl...@apache.org on 2026/05/10 14:27:14 UTC

(creadur-rat) 02/02: created testhelper and updated tests

This is an automated email from the ASF dual-hosted git repository.

Claudenw pushed a commit to branch RAT-541_create-testhelper_data-package
in repository https://gitbox.apache.org/repos/asf/creadur-rat.git

commit 2a8c2f71997bf2d8b5a4c1a8ee01a15d63144ff3
Author: Claude Warren <cl...@xenei.com>
AuthorDate: Sun May 10 15:26:40 2026 +0100

    created testhelper and updated tests
---
 .../src/main/java/org/apache/rat/Report.java       |   63 +-
 .../java/org/apache/rat/ReportConfiguration.java   |  201 ++-
 .../src/main/java/org/apache/rat/Reporter.java     |  294 +++--
 .../main/java/org/apache/rat/commandline/Arg.java  |    7 +-
 .../org/apache/rat/commandline/StyleSheets.java    |   37 +-
 .../rat/config/exclusion/ExclusionProcessor.java   |   84 +-
 .../rat/configuration/XMLConfigurationReader.java  |    4 +-
 .../org/apache/rat/license/LicenseSetFactory.java  |   30 +
 .../java/org/apache/rat/report/IReportable.java    |    2 +-
 .../apache/rat/report/claim/ClaimStatistic.java    |  115 ++
 .../java/org/apache/rat/walker/FileListWalker.java |    4 +-
 .../apache/rat/walker/IReportableListWalker.java   |    4 +-
 .../main/java/org/apache/rat/walker/Walker.java    |    2 +-
 .../src/main/resources/org/apache/rat/xhtml5.xsl   |  354 ++++++
 .../java/org/apache/rat/OptionCollectionTest.java  |    2 +-
 .../org/apache/rat/ReportConfigurationTest.java    |  101 +-
 .../org/apache/rat/ReporterOptionsProvider.java    |  303 ++---
 .../java/org/apache/rat/ReporterOptionsTest.java   |   12 +-
 .../src/test/java/org/apache/rat/ReporterTest.java |  108 +-
 .../configuration/XMLConfigurationReaderTest.java  |   17 +-
 .../test/AbstractConfigurationOptionsProvider.java |    4 +-
 .../org/apache/rat/testhelpers/BaseOption.java     |   25 +
 .../rat/testhelpers/BaseOptionCollection.java      |   24 +
 .../org/apache/rat/testhelpers/TestingLog.java     |   11 +-
 .../testhelpers/data/AbstractTestDataProvider.java |    8 +-
 .../org/apache/rat/testhelpers/data/DataUtils.java |  142 +++
 .../testhelpers/data/ReportTestDataProvider.java   | 1282 ++++++++++++++++++++
 .../org/apache/rat/testhelpers/data/TestData.java  |   18 +-
 .../apache/rat/testhelpers/data/ValidatorData.java |   17 -
 .../rat/anttasks/ResourceCollectionContainer.java  |    2 +-
 30 files changed, 2830 insertions(+), 447 deletions(-)

diff --git a/apache-rat-core/src/main/java/org/apache/rat/Report.java b/apache-rat-core/src/main/java/org/apache/rat/Report.java
index 5f5f4cb3..5d9c914f 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/Report.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/Report.java
@@ -19,8 +19,13 @@
 package org.apache.rat;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
 
 import org.apache.commons.cli.Options;
+import org.apache.commons.io.function.IOSupplier;
+import org.apache.rat.commandline.ArgumentContext;
 import org.apache.rat.document.RatDocumentAnalysisException;
 import org.apache.rat.help.Help;
 import org.apache.rat.utils.DefaultLog;
@@ -42,34 +47,66 @@ public final class Report {
     public static void main(final String[] args) throws Exception {
         DefaultLog.getInstance().info(new VersionInfo().toString());
 
+
         if (args == null || args.length == 0) {
             DefaultLog.getInstance().info("Please use the \"--help\" option to see a " +
                     "list of valid commands and options, as you did not provide any arguments.");
             System.exit(0);
         }
 
-        ReportConfiguration configuration = OptionCollection.parseCommands(new File("."), args, Report::printUsage);
-        if (configuration != null) {
-            configuration.validate(DefaultLog.getInstance()::error);
-            Reporter reporter = new Reporter(configuration);
-            reporter.output();
-            reporter.writeSummary(DefaultLog.getInstance().asWriter());
+        Reporter.Output result = generateReport(CLIOptionCollection.INSTANCE, new File("."), args);
+        if (result != null) {
+            result.writeSummary(DefaultLog.getInstance().asWriter());
 
-            if (configuration.getClaimValidator().hasErrors()) {
-                configuration.getClaimValidator().logIssues(reporter.getClaimsStatistic());
+            if (result.getConfiguration().getClaimValidator().hasErrors()) {
+                result.getConfiguration().getClaimValidator().logIssues(result.getStatistic());
                 throw new RatDocumentAnalysisException(format("Issues with %s",
                         String.join(", ",
-                                configuration.getClaimValidator().listIssues(reporter.getClaimsStatistic()))));
+                                result.getConfiguration().getClaimValidator().listIssues(result.getStatistic()))));
             }
         }
     }
 
     /**
-     * Prints the usage message on {@code System.out}.
-     * @param opts The defined options.
+     * Prints the usage message on the specified output stream.
+     * @param out The OutputStream supplier
      */
-    private static void printUsage(final Options opts) {
-        new Help(System.out).printUsage(opts);
+    private static void printUsage(final Options options, final IOSupplier<OutputStream> out) {
+        try (OutputStream stream = out.get();
+             PrintWriter writer = new PrintWriter(stream)) {
+            new Help(writer).printUsage(options);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Generates the report
+     * @param workingDirectory the directory that we are executing in
+     * @param args the arguments from the command line.
+     * @return The Client output.
+     * @throws Exception on error.
+     */
+    static Reporter.Output generateReport(final CLIOptionCollection optionCollection, final File workingDirectory, final String[] args) throws Exception {
+        Reporter.Output output = null;
+        OptionCollectionParser optionParser = new OptionCollectionParser(optionCollection);
+        ArgumentContext argumentContext = optionParser.parseCommands(workingDirectory, args);
+        ReportConfiguration configuration = argumentContext.getConfiguration();
+        if (configuration != null) {
+            if (argumentContext.getCommandLine().hasOption(CLIOptionCollection.HELP)) {
+                printUsage(optionCollection.getOptions(), argumentContext.getConfiguration().getOutput());
+            } else if (!configuration.hasSource()) {
+                String msg = "No directories or files specified for scanning. Did you forget to close a multi-argument option?";
+                DefaultLog.getInstance().error(msg);
+                printUsage(optionCollection.getOptions(), argumentContext.getConfiguration().getOutput());
+            } else {
+                configuration.validate();
+                Reporter reporter = new Reporter(configuration);
+                output = reporter.execute();
+                output.format(configuration);
+            }
+        }
+        return output;
     }
 
     private Report() {
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java b/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java
index 799dfaf5..82ce4920 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/ReportConfiguration.java
@@ -34,18 +34,25 @@ import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.SortedSet;
-import java.util.function.Consumer;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.commons.io.function.IOSupplier;
 import org.apache.commons.io.output.CloseShieldOutputStream;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.rat.analysis.IHeaderMatcher;
+import org.apache.rat.api.RatException;
 import org.apache.rat.commandline.StyleSheets;
 import org.apache.rat.config.AddLicenseHeaders;
 import org.apache.rat.config.exclusion.ExclusionProcessor;
 import org.apache.rat.config.exclusion.StandardCollection;
 import org.apache.rat.config.results.ClaimValidator;
+import org.apache.rat.configuration.XMLConfigurationReader;
 import org.apache.rat.configuration.builders.AnyBuilder;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.document.DocumentNameMatcher;
@@ -55,11 +62,17 @@ import org.apache.rat.license.ILicenseFamily;
 import org.apache.rat.license.LicenseSetFactory;
 import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
 import org.apache.rat.report.IReportable;
+import org.apache.rat.report.RatReport;
+import org.apache.rat.report.claim.ClaimStatistic;
+import org.apache.rat.report.xml.writer.IXmlWriter;
+import org.apache.rat.report.xml.writer.XmlWriter;
 import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.Log.Level;
 import org.apache.rat.utils.ReportingSet;
 import org.apache.rat.walker.FileListWalker;
 import org.apache.rat.walker.IReportableListWalker;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
 
 /**
  * A configuration object is used by the front end to invoke the
@@ -68,6 +81,8 @@ import org.apache.rat.walker.IReportableListWalker;
  */
 public class ReportConfiguration {
 
+    /** The IODescriptor for System.out */
+    public static final IODescriptor<OutputStream> SYSTEM_OUT = new  IODescriptor<>("System.out", () -> CloseShieldOutputStream.wrap(System.out));
     /**
      * The styles of processing for various categories of documents.
      */
@@ -117,11 +132,11 @@ public class ReportConfiguration {
     /**
      * The IOSupplier that provides the output stream to write the report to.
      */
-    private IOSupplier<OutputStream> out;
+    private IODescriptor<OutputStream> out;
     /**
      * The IOSupplier that provides the stylesheet to style the XML output.
      */
-    private IOSupplier<InputStream> styleSheet;
+    private IODescriptor<InputStream> styleSheet;
 
     /**
      * A list of files to read file names from.
@@ -176,6 +191,10 @@ public class ReportConfiguration {
         reportables = new ArrayList<>();
     }
 
+    public Serde serde() {
+        return new Serde();
+    }
+
     /**
      * Report the excluded files to the appendable object.
      * @param appendable the appendable object to write to.
@@ -427,7 +446,7 @@ public class ReportConfiguration {
     }
 
     /**
-     * Get the DocumentNameMatcher that excludes files found in the directory tree..
+     * Get the DocumentNameMatcher that excludes files found in the directory tree.
      * @param baseDir the DocumentName for the base directory.
      * @return the DocumentNameMatcher for the base directory.
      */
@@ -441,6 +460,15 @@ public class ReportConfiguration {
      * the report with.
      */
     public IOSupplier<InputStream> getStyleSheet() {
+        return styleSheet.ioSupplier();
+    }
+
+    /**
+     * Gets the IODescriptor of the style sheet.
+     * @return the Supplier of the InputStream that is the XSLT style sheet to style
+     * the report with.
+     */
+    public IODescriptor<InputStream> getStyleSheetDescriptor() {
         return styleSheet;
     }
 
@@ -450,7 +478,7 @@ public class ReportConfiguration {
      * multiple times.
      * @param styleSheet the XSLT style sheet to style the report with.
      */
-    public void setStyleSheet(final IOSupplier<InputStream> styleSheet) {
+    public void setStyleSheet(final IODescriptor<InputStream> styleSheet) {
         this.styleSheet = styleSheet;
     }
 
@@ -462,7 +490,7 @@ public class ReportConfiguration {
      */
     public void setFrom(final Defaults defaults) {
         licenseSetFactory.add(defaults.getLicenseSetFactory());
-        if (getStyleSheet() == null) {
+        if (getStyleSheetDescriptor() == null) {
             setStyleSheet(StyleSheets.PLAIN.getStyleSheet());
         }
         defaults.getStandardExclusion().forEach(this::addExcludedCollection);
@@ -498,19 +526,18 @@ public class ReportConfiguration {
      */
     public void setStyleSheet(final URL styleSheet) {
         Objects.requireNonNull(styleSheet, "Stylesheet file must not be null");
-        setStyleSheet(styleSheet::openStream);
+        setStyleSheet(new IODescriptor<>(styleSheet.toString(), styleSheet::openStream));
     }
 
     /**
      * Sets the supplier for the output stream. The supplier may be called multiple
      * times to provide the stream. Suppliers should prepare streams that are
      * appended to and that can be closed. If an {@code OutputStream} should not be
-     * closed consider wrapping it in a {@code CloseShieldOutputStream}
+     * closed consider wrapping it in a {@code NoCloseOutputStream}
      * @param out The OutputStream supplier that provides the output stream to write
      * the report to. A null value will use System.out.
-     * @see CloseShieldOutputStream
      */
-    public void setOut(final IOSupplier<OutputStream> out) {
+    public void setOut(final IODescriptor<OutputStream> out) {
         this.out = out;
     }
 
@@ -518,7 +545,7 @@ public class ReportConfiguration {
      * Sets the OutputStream supplier to use the specified file. The file may be
      * opened and closed several times. File is deleted first and then may be
      * repeatedly opened in append mode.
-     * @see #setOut(IOSupplier)
+     * @see #setOut(IODescriptor)
      * @param file The file to create the supplier with.
      */
     public void setOut(final File file) {
@@ -534,7 +561,7 @@ public class ReportConfiguration {
         if (!parent.mkdirs() && !parent.isDirectory()) {
             DefaultLog.getInstance().warn("Unable to create directory: " + file.getParentFile());
         }
-        setOut(() -> new FileOutputStream(file, true));
+        setOut(new IODescriptor<>(file.toString(), () -> new FileOutputStream(file, true)));
     }
 
     /**
@@ -543,7 +570,15 @@ public class ReportConfiguration {
      * @return The supplier of the output stream to write the report to.
      */
     public IOSupplier<OutputStream> getOutput() {
-        return out == null ? () -> CloseShieldOutputStream.wrap(System.out) : out;
+        return getOutputDescriptor().ioSupplier();
+    }
+
+    /**
+     * Returns the output IODescriptor.
+     * @return The IODescriptor of the output stream to write the report to.
+     */
+    public IODescriptor<OutputStream> getOutputDescriptor() {
+        return out == null ? SYSTEM_OUT : out;
     }
 
     /**
@@ -827,19 +862,145 @@ public class ReportConfiguration {
 
     /**
      * Validates that the configuration is valid.
-     * @param logger String consumer to log warning messages to.
      * @throws ConfigurationException on configuration error.
      */
-    public void validate(final Consumer<String> logger) {
+    public void validate() {
         if (!hasSource()) {
             String msg = "At least one source must be specified";
-            logger.accept(msg);
+            DefaultLog.getInstance().error(msg);
             throw new ConfigurationException(msg);
         }
-        if (licenseSetFactory.getLicenses(LicenseFilter.ALL).isEmpty()) {
-            String msg = "You must specify at least one license";
-            logger.accept(msg);
-            throw new ConfigurationException(msg);
+        licenseSetFactory.validate();
+    }
+
+    public class Serde {
+        /**
+         * Writes the configuration as an XML document to the appendable.
+         *
+         * @param appendable the Appendable to write to.
+         * @throws IOException on error.
+         */
+        public void serialize(final Appendable appendable) throws IOException {
+            try (IXmlWriter writer = new XmlWriter(appendable)) {
+                writer.openElement("ReportConfiguration")
+                        .attribute("addingLicenses", Boolean.toString(addingLicenses))
+                        .attribute("addingLicensesForced", Boolean.toString(addingLicensesForced))
+                        .attribute("listFamilies", listFamilies.name())
+                        .attribute("listLicenses", listLicenses.name())
+                        .attribute("dryRun", Boolean.toString(dryRun))
+                        .attribute("archiveProcessing", getArchiveProcessing().name())
+                        .attribute("standardProcessing", getStandardProcessing().name())
+                        .attribute("stylesheet", styleSheet.name())
+                        .attribute("output", out.name());
+                if (StringUtils.isNotEmpty(copyrightMessage)) {
+                    writer.openElement("copyrightMessage").content(copyrightMessage).closeElement();
+                }
+                writer.openElement("sources");
+                for (File f : sources) {
+                    writer.openElement("source").attribute("name", f.getName()).closeElement();
+                }
+                writer.closeElement("sources").openElement("reportables");
+                for (IReportable reportable : reportables) {
+                    writer.openElement("reportable")
+                            .attribute("baseName", reportable.name().getBaseName())
+                            .attribute("name", reportable.name().toString())
+                            .attribute("class", reportable.getClass().getName()).closeElement();
+                }
+                writer.closeElement();
+
+                exclusionProcessor.serde().serialize(writer);
+
+                writer.openElement("claimValidator");
+                for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) {
+                    writer.openElement("claimCounter")
+                            .attribute("name", counter.name()).attribute("min", Integer.toString(claimValidator.getMin(counter)))
+                            .attribute("max", Integer.toString(claimValidator.getMax(counter))).closeElement();
+                }
+                writer.closeElement();
+            } catch (IOException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new IOException(e);
+            }
+        }
+
+        public void deserialize(final IOSupplier<InputStream> inputStreamSupplier, final DocumentName workingDirectory) throws IOException {
+            DocumentBuilder builder;
+            try {
+                builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+            } catch (ParserConfigurationException e) {
+                throw new ConfigurationException("Unable to create DOM builder", e);
+            }
+            org.w3c.dom.Document document;
+            try (InputStream stream = inputStreamSupplier.get()) {
+                document = builder.parse(stream);
+            } catch (SAXException e) {
+                throw new IOException("Unable to read input", e);
+            }
+            Node node = document.getDocumentElement();
+            if (!node.getNodeName().equals("ReportConfiguration")) {
+                throw new IOException("Invalid ReportConfiguration");
+            }
+            Map<String, String> attributes = XMLConfigurationReader.attributes(node);
+            addingLicensesForced = Boolean.parseBoolean(attributes.get("addingLicensesForced"));
+            listFamilies = LicenseFilter.valueOf(attributes.get("listFamilies"));
+            listLicenses = LicenseFilter.valueOf(attributes.get("listLicenses"));
+            dryRun = Boolean.parseBoolean(attributes.get("dryRun"));
+            archiveProcessing = Processing.valueOf(attributes.get("archiveProcessing"));
+            standardProcessing = Processing.valueOf(attributes.get("standardProcessing"));
+            String styleName = attributes.get("stylesheet");
+            if (styleName != null) {
+                styleSheet = StyleSheets.getStyleSheet(styleName, workingDirectory);
+            }
+            String outputName = attributes.get("output");
+            if (outputName != null) {
+                if (outputName.equals(ReportConfiguration.SYSTEM_OUT.name())) {
+                    out = ReportConfiguration.SYSTEM_OUT;
+                } else {
+                    out = IODescriptor.output(outputName, workingDirectory);
+                }
+            }
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("source"), lNode -> {
+                Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
+                addSource(new File(nAttributes.get("name")));
+            });
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("reportable"), lNode -> {
+                Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
+                DocumentName documentName = DocumentName.builder().setBaseName(nAttributes.get("baseName"))
+                        .setName(nAttributes.get("name")).build();
+                addSource(new DeserializedReportable(documentName));
+            });
+
+            exclusionProcessor.serde().deserialize(document.getElementsByTagName("ExclusionProcessor").item(0));
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("claimCounter"), lNode -> {
+                Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
+                ClaimStatistic.Counter counter = ClaimStatistic.Counter.valueOf(nAttributes.get("name"));
+                claimValidator.setMin(counter, Integer.parseInt(nAttributes.get("min")));
+                claimValidator.setMax(counter, Integer.parseInt(nAttributes.get("max")));
+            });
+        }
+    }
+
+    private record DeserializedReportable(DocumentName name) implements IReportable {
+        @Override
+        public void run(final RatReport report) throws RatException {
+            throw new RatException("Attempt to run a deserialized reportable");
         }
     }
+
+    public record IODescriptor<T>(String name, IOSupplier<T> ioSupplier) {
+        /**
+         * Creates an output IODescriptor for the file name within the working directory.
+         * @param name the name of the file to open.
+         * @param workingDirectory the working directory for the file.
+         * @return the Output IODescriptor.
+         */
+        static IODescriptor<OutputStream> output(final String name, final DocumentName workingDirectory) {
+            DocumentName docName = workingDirectory.resolve(name);
+            return new IODescriptor<OutputStream>(name, () -> new FileOutputStream(docName.asFile()));
+        }
+    };
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/Reporter.java b/apache-rat-core/src/main/java/org/apache/rat/Reporter.java
index 2e58c582..3159c926 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/Reporter.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/Reporter.java
@@ -18,17 +18,19 @@
  */
 package org.apache.rat;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.Writer;
+import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 
+import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.transform.OutputKeys;
 import javax.xml.transform.Transformer;
 import javax.xml.transform.TransformerException;
@@ -39,6 +41,7 @@ import javax.xml.transform.stream.StreamSource;
 
 import org.apache.commons.io.function.IOSupplier;
 import org.apache.rat.api.RatException;
+import org.apache.rat.document.DocumentName;
 import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
 import org.apache.rat.report.RatReport;
 import org.apache.rat.report.claim.ClaimStatistic;
@@ -46,6 +49,8 @@ import org.apache.rat.report.xml.XmlReportFactory;
 import org.apache.rat.report.xml.writer.IXmlWriter;
 import org.apache.rat.report.xml.writer.XmlWriter;
 import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
 
 /**
  * Class that executes the report as defined in a {@link ReportConfiguration} and stores
@@ -56,15 +61,14 @@ public class Reporter {
     /**  Format used for listing licenses. */
     private static final String LICENSE_FORMAT = "%s:\t%s%n\t\t%s%n";
 
-    /** The XML output document */
-    private Document document;
-
-    /** Statistics generated as the report was built */
-    private ClaimStatistic statistic;
-
     /** The configuration for the report */
     private final ReportConfiguration configuration;
 
+    /**
+     * The output from the execution.
+     */
+    private Output output;
+
     /**
      * Create the reporter.
      *
@@ -76,83 +80,39 @@ public class Reporter {
 
     /**
      * Executes the report and builds the output.
-     * This method will build the internal XML document if it does not already exist.
-     * If this method or either of the {@link #output()} methods have already been called this method will return
-     * the previous results.
-     * @return the claim statistics.
+     * @return the Output object.
      * @throws RatException on error.
      */
-    public ClaimStatistic execute() throws RatException  {
-        if (document == null || statistic == null) {
-            try {
-                if (configuration.hasSource()) {
-                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-                    Writer outputWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
-                    try (IXmlWriter writer = new XmlWriter(outputWriter)) {
-                        statistic = new ClaimStatistic();
-                        RatReport report = XmlReportFactory.createStandardReport(writer, statistic, configuration);
-                        report.startReport();
-                        configuration.getSources().build().run(report);
-                        report.endReport();
-                    }
-                    InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
-                    document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream);
-                } else {
-                    document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
-                    statistic = new ClaimStatistic();
+    public Output execute() throws RatException {
+        try {
+            Output.Builder builder = Output.builder().configuration(configuration);
+            if (configuration.hasSource()) {
+                StringBuilder sb = new StringBuilder();
+                try (IXmlWriter writer = new XmlWriter(sb)) {
+                    writer.startDocument();
+                    ClaimStatistic statistic = new ClaimStatistic();
+                    builder.statistic(statistic);
+                    RatReport report = XmlReportFactory.createStandardReport(writer, statistic, configuration);
+                    report.startReport();
+                    configuration.getSources().build().run(report);
+                    report.endReport();
+                    InputSource inputSource = new InputSource(new StringReader(sb.toString()));
+                    builder.document(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource));
                 }
-            }  catch (Exception e) {
-                throw RatException.makeRatException(e);
             }
+            this.output = builder.build();
+            return output;
+        } catch (Exception e) {
+            throw RatException.makeRatException(e);
         }
-        return statistic;
     }
 
     /**
-     * Get the claim statistics from the run.
-     *
-     * @return the claim statistics.
+     * Gets the output from the last {@link #execute} call or {@code null} if {@link #execute} has not been called.
+     * @return the output
      */
-    public ClaimStatistic getClaimsStatistic() {
-        return statistic;
-    }
-
-    /**
-     * Outputs the report using the stylesheet and output specified in the configuration.
-     * @return the Claim statistic from the run.
-     * @throws RatException on error.
-     */
-    public ClaimStatistic output() throws RatException {
-        return output(configuration.getStyleSheet(), configuration.getOutput());
-    }
-
-    /**
-     * Outputs the report to the specified output using the stylesheet. It is safe to call this method more than once
-     * in order to generate multiple reports from the same run.
-     *
-     * @param stylesheet the style sheet to use for XSLT formatting.
-     * @param output the output stream to write to.
-     * @return the Claim statistic for the run.
-     * @throws RatException on error.
-     */
-    public ClaimStatistic output(final IOSupplier<InputStream> stylesheet, final IOSupplier<OutputStream> output) throws RatException {
-        ClaimStatistic result = execute();
-        TransformerFactory tf = TransformerFactory.newInstance();
-        Transformer transformer;
-        try (OutputStream out = output.get();
-             InputStream styleIn = stylesheet.get()) {
-            transformer = tf.newTransformer(new StreamSource(styleIn));
-            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
-            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
-            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
-            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
-            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
-            transformer.transform(new DOMSource(document),
-                    new StreamResult(new OutputStreamWriter(out, StandardCharsets.UTF_8)));
-            return result;
-        } catch (TransformerException | IOException e) {
-            throw new RatException(e);
-        }
+    public Output getOutput() {
+        return output;
     }
 
     /**
@@ -172,24 +132,172 @@ public class Reporter {
     }
 
     /**
-     * Writes a text summary of issues with the run.
-     * @param appendable the appendable to write to.
-     * @throws IOException on error.
+     * The output from a report run.
      */
-    public void writeSummary(final Appendable appendable) throws IOException {
-        appendable.append("RAT summary:").append(System.lineSeparator());
-        for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) {
-            appendable.append("  ").append(counter.displayName()).append(":  ")
-                    .append(Integer.toString(getClaimsStatistic().getCounter(counter)))
-                    .append(System.lineSeparator());
+    public static final class Output {
+        /** The XML output document */
+        private final Document document;
+        /**
+         * The claim statics from the execution that generated the document.
+         * May be empty if the Document was read from disk.
+         */
+        private final ClaimStatistic statistic;
+        /**
+         * The configuration that generated the document
+         */
+        private final ReportConfiguration configuration;
+
+        /**
+         * Create an output with statistics.
+         * @param builder the Builder
+         */
+        private Output(final Builder builder) {
+            this.document = builder.document;
+            this.statistic = builder.statistic == null ? new ClaimStatistic() : builder.statistic;
+            this.configuration = builder.configuration == null ? new ReportConfiguration() : builder.configuration;
         }
-    }
 
-    /**
-     * Gets the document that was generated during execution.
-     * @return the document that was generated during execution.
-     */
-    public Document getDocument() {
-        return document;
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * Gets the document that was generated during execution.
+         * @return the document that was generated during execution.
+         */
+        public Document getDocument() {
+            return document;
+        }
+
+        public ClaimStatistic getStatistic() {
+            return statistic;
+        }
+
+        public ReportConfiguration getConfiguration() {
+            return configuration;
+        }
+        /**
+         * Formats the report to the output and using the stylesheet found in the report configuration.
+         *
+         * @param config s RAT report configuration.
+         * @throws RatException on error.
+         */
+        public void format(final ReportConfiguration config) throws RatException {
+            format(config.getStyleSheet(), config.getOutput());
+        }
+
+        /**
+         * Formats the report to the specified output using the stylesheet. It is safe to call this method more than once
+         * in order to generate multiple reports from the same run.
+         *
+         * @param stylesheet the style sheet to use for XSLT formatting.
+         * @param output the output stream to write to.
+         * @throws RatException on error.
+         */
+        public void format(final IOSupplier<InputStream> stylesheet, final IOSupplier<OutputStream> output) throws RatException {
+            TransformerFactory tf = TransformerFactory.newInstance();
+            Transformer transformer;
+            try (OutputStream out = output.get();
+                 InputStream styleIn = stylesheet.get()) {
+                transformer = tf.newTransformer(new StreamSource(styleIn));
+                transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+                transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+                transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+                transformer.transform(new DOMSource(document),
+                        new StreamResult(new OutputStreamWriter(out, StandardCharsets.UTF_8)));
+            } catch (TransformerException | IOException e) {
+                throw new RatException(e);
+            }
+        }
+
+        /**
+         * Writes a text summary of issues with the run.
+         * @param appendable the appendable to write to.
+         * @throws IOException on error.
+         */
+        public void writeSummary(final Appendable appendable) throws IOException {
+            appendable.append("RAT summary:").append(System.lineSeparator());
+            for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) {
+                appendable.append("  ").append(counter.displayName()).append(":  ")
+                        .append(Integer.toString(statistic.getCounter(counter)))
+                        .append(System.lineSeparator());
+            }
+        }
+
+        public static final class Builder {
+            /** The document that was generated */
+            private Document document;
+            /**
+             * The claim statistic from the execution that generated the document.
+             * May be empty if the Document was read from disk.
+             */
+            private ClaimStatistic statistic;
+            /**
+             * The configuration that generated the document
+             */
+            private ReportConfiguration configuration;
+
+            public Builder document(final Document document) {
+                this.document = document;
+                return this;
+            }
+
+            public Builder document(final String fileName, final DocumentName workingDirectory) {
+                DocumentBuilder builder;
+                try {
+                    builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+                } catch (ParserConfigurationException e) {
+                    throw new ConfigurationException("Unable to create DOM builder", e);
+                }
+
+                File inputFile = workingDirectory.resolve(fileName).asFile();
+                try (InputStream inputStream = new FileInputStream(inputFile)) {
+                    this.document = builder.parse(inputStream);
+                } catch (SAXException | IOException e) {
+                    throw new ConfigurationException("Unable to read file: " + inputFile, e);
+                }
+                return this;
+            }
+
+            public Output build() {
+                return new Output(this);
+            }
+
+            public Builder statistic(final ClaimStatistic statistic) {
+                this.statistic = statistic;
+                return this;
+            }
+
+            public Builder statistic(final String fileName, final DocumentName workingDirectory) {
+                File sourceFile = workingDirectory.resolve(fileName).asFile();
+                try {
+                    ClaimStatistic statistic = new ClaimStatistic();
+                    statistic.serde().deserialize(() -> new FileInputStream(sourceFile));
+                    this.statistic = statistic;
+                    return this;
+                } catch (IOException e) {
+                    throw new ConfigurationException("Unable to read file: " + sourceFile, e);
+                }
+            }
+
+            public Builder configuration(final ReportConfiguration configuration) {
+                this.configuration = configuration;
+                return this;
+            }
+
+            public Builder configuration(final String fileName, final DocumentName workingDirectory) {
+                File configurationFile = workingDirectory.resolve(fileName).asFile();
+                try {
+                    ReportConfiguration config = new ReportConfiguration();
+                    config.serde().deserialize(() -> new FileInputStream(configurationFile), workingDirectory);
+                    this.configuration = config;
+                    return this;
+                } catch (IOException e) {
+                    throw new ConfigurationException("Unable to read file: " + configurationFile, e);
+                }
+            }
+        }
     }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java
index 9edd3483..4f2696c7 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java
@@ -38,7 +38,6 @@ import org.apache.commons.cli.OptionGroup;
 import org.apache.commons.cli.Options;
 import org.apache.commons.cli.ParseException;
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.output.CloseShieldOutputStream;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.rat.ConfigurationException;
 import org.apache.rat.Defaults;
@@ -519,14 +518,14 @@ public enum Arg {
                 if ("x".equals(key)) {
                     // display deprecated message.
                     context.getCommandLine().hasOption("x");
-                    context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml"));
+                    context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml", context.getWorkingDirectory()));
                 } else {
                     String[] style = context.getCommandLine().getOptionValues(selected);
                     if (style.length != 1) {
                         DefaultLog.getInstance().error("Please specify a single stylesheet");
                         throw new ConfigurationException("Please specify a single stylesheet");
                     }
-                    context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0]));
+                    context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0], context.getWorkingDirectory()));
                 }
             }),
 
@@ -625,7 +624,7 @@ public enum Arg {
                 } catch (ParseException e) {
                     // we write to system out by default.
                     context.logParseException(e, selected, "System.out");
-                    context.getConfiguration().setOut(() -> CloseShieldOutputStream.wrap(System.out)); // NOSONAR
+                    context.getConfiguration().setOut(ReportConfiguration.SYSTEM_OUT);
                 }
             }),
 
diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java
index c73a9e4e..f9e47cc2 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/StyleSheets.java
@@ -21,12 +21,11 @@ package org.apache.rat.commandline;
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Objects;
 
-import org.apache.commons.io.function.IOSupplier;
 import org.apache.rat.ConfigurationException;
+import org.apache.rat.ReportConfiguration;
+import org.apache.rat.document.DocumentName;
 
 import static java.lang.String.format;
 
@@ -49,7 +48,11 @@ public enum StyleSheets {
     /**
      * The plain style sheet. The current default.
      */
-    XML("xml", "Produces output in pretty-printed XML.");
+    XML("xml", "Produces output in pretty-printed XML."),
+    /**
+     * Official HTML5 stylesheet.
+     */
+    XHTML5("xhtml5", "Produces a HTML5 report");
     /**
      * The name of the style sheet. Must map to bundled resource xslt file
      */
@@ -70,27 +73,29 @@ public enum StyleSheets {
     }
 
     /**
-     * Gets the IOSupplier for a style sheet.
-     * @return an IOSupplier for the sheet.
+     * Gets the IODescriptor for a style sheet.
+     * @return an IODescriptor for the sheet.
      */
-    public IOSupplier<InputStream> getStyleSheet() {
-        return Objects.requireNonNull(StyleSheets.class.getClassLoader().getResource(format("org/apache/rat/%s.xsl", name)),
-                "missing stylesheet: " + name)::openStream;
+    public ReportConfiguration.IODescriptor<InputStream> getStyleSheet() {
+        URL url = StyleSheets.class.getClassLoader().getResource(format("org/apache/rat/%s.xsl", name));
+        Objects.requireNonNull(url, "missing stylesheet: " + name);
+        return new ReportConfiguration.IODescriptor<>(name, url::openStream);
     }
 
     /**
-     * Gets the IOSupplier for a style sheet.
+     * Gets the IODescriptor for a style sheet.
      * @param name the short name for or the path to a style sheet.
-     * @return the IOSupplier for the style sheet.
+     * @param workingDirectory the working directory to resolve the name against.
+     * @return the IODescriptor for the style sheet.
      */
-    public static IOSupplier<InputStream> getStyleSheet(final String name) {
+    public static ReportConfiguration.IODescriptor<InputStream> getStyleSheet(final String name, final DocumentName workingDirectory) {
         URL url = StyleSheets.class.getClassLoader().getResource(format("org/apache/rat/%s.xsl", name));
         if (url != null) {
-            return url::openStream;
+            return new ReportConfiguration.IODescriptor<>(name, url::openStream);
         }
-        Path p = Paths.get(name);
-        if (p.toFile().exists()) {
-            return () -> Files.newInputStream(p);
+        DocumentName xslt = workingDirectory.resolve(name);
+        if (xslt.asFile().exists()) {
+            return new ReportConfiguration.IODescriptor<>(xslt.toString(), () -> Files.newInputStream(xslt.asFile().toPath()));
         }
         throw new ConfigurationException(format("Stylesheet file '%s' not found", name));
     }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
index aa2e3cc1..6cfeffc0 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/ExclusionProcessor.java
@@ -22,15 +22,22 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.rat.configuration.XMLConfigurationReader;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.document.DocumentNameMatcher;
+import org.apache.rat.report.xml.writer.IXmlWriter;
 import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.ExtendedIterator;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
 import static java.lang.String.format;
 
@@ -74,7 +81,13 @@ public class ExclusionProcessor {
         excludedCollections = new HashSet<>();
     }
 
-    /** Reset the {@link #lastMatcher} and {@link #lastMatcherBaseDir} to start again */
+    public Serde serde() {
+        return new Serde();
+    }
+
+    /**
+     * Reset the {@link #lastMatcher} and {@link #lastMatcherBaseDir} to start again
+     */
     private void resetLastMatcher() {
         lastMatcher = null;
         lastMatcherBaseDir = null;
@@ -337,4 +350,73 @@ public class ExclusionProcessor {
         }
     }
 
+    public class Serde {
+        public void serialize(final IXmlWriter writer) throws IOException {
+            writer.openElement("ExclusionProcessor");
+
+            for (String pattern : excludedPatterns) {
+                writer.openElement("excludedPattern").attribute("pattern", pattern).closeElement();
+            }
+            for (StandardCollection obj : excludedCollections) {
+                writer.openElement("excludedCollection").attribute("name", obj.name()).closeElement();
+            }
+            for (DocumentNameMatcher obj : excludedPaths) {
+                writer.openElement("excludedPath").attribute("name", obj.toString()).closeElement();
+            }
+
+            for (String pattern : includedPatterns) {
+                writer.openElement("includedPattern").attribute("pattern", pattern).closeElement();
+            }
+            for (StandardCollection obj : includedCollections) {
+                writer.openElement("includedCollection").attribute("name", obj.name()).closeElement();
+            }
+            for (DocumentNameMatcher obj : includedPaths) {
+                writer.openElement("includedPath").attribute("name", obj.toString()).closeElement();
+            }
+
+            for (StandardCollection obj : fileProcessors) {
+                writer.openElement("fileProcessor").attribute("name", obj.name()).closeElement();
+            }
+            writer.closeElement();
+
+        }
+
+        public void deserialize(final Node n) {
+            final NodeList children = n.getChildNodes();
+            for (int i = 0; i < children.getLength(); i++) {
+                Node child = children.item(i);
+                Map<String, String> attributes = XMLConfigurationReader.attributes(child);
+                StandardCollection collection;
+                switch (child.getNodeName()) {
+                    case "excludedPattern":
+                        excludedPatterns.add(attributes.get("pattern"));
+                        break;
+                    case "excludedCollection":
+                        collection = StandardCollection.valueOf(attributes.get("name"));
+                        excludedCollections.add(collection);
+                        break;
+                    case "excludedPath":
+                        excludedPaths.add(new DocumentNameMatcher(attributes.get("name"),
+                                (Predicate<DocumentName>) x -> {
+                                    throw new NotImplementedException("Deserialized ExclusionProcessor can not evaluate paths");
+                                }));
+                        break;
+                    case "includedPattern":
+                        includedPatterns.add(attributes.get("pattern"));
+                        break;
+                    case "includedCollection":
+                        collection = StandardCollection.valueOf(attributes.get("name"));
+                        includedCollections.add(collection);
+                        break;
+                    case "includedPath":
+                        excludedPaths.add(new DocumentNameMatcher(attributes.get("name"),
+                                (Predicate<DocumentName>) x -> {
+                                    throw new NotImplementedException("Deserialized ExclusionProcessor can not evaluate paths");
+                                }));
+                        break;
+                }
+
+            }
+        }
+    }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java b/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java
index 4d3e1a88..51ef1c1a 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/configuration/XMLConfigurationReader.java
@@ -191,7 +191,7 @@ public final class XMLConfigurationReader implements LicenseReader, MatcherReade
      * @param list the NodeList to process
      * @param consumer the consumer to apply to each node in the list.
      */
-    private void nodeListConsumer(final NodeList list, final Consumer<Node> consumer) {
+    public static void nodeListConsumer(final NodeList list, final Consumer<Node> consumer) {
         for (int i = 0; i < list.getLength(); i++) {
             consumer.accept(list.item(i));
         }
@@ -217,7 +217,7 @@ public final class XMLConfigurationReader implements LicenseReader, MatcherReade
      * @param node The node to process
      * @return the map of attributes on the node.
      */
-    private Map<String, String> attributes(final Node node) {
+    public static Map<String, String> attributes(final Node node) {
         NamedNodeMap nnm = node.getAttributes();
         Map<String, String> result = new HashMap<>();
         for (int i = 0; i < nnm.getLength(); i++) {
diff --git a/apache-rat-core/src/main/java/org/apache/rat/license/LicenseSetFactory.java b/apache-rat-core/src/main/java/org/apache/rat/license/LicenseSetFactory.java
index 6fe00db5..07f27891 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/license/LicenseSetFactory.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/license/LicenseSetFactory.java
@@ -20,13 +20,18 @@ package org.apache.rat.license;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Optional;
+import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
+import org.apache.rat.ConfigurationException;
 import org.apache.rat.analysis.IHeaderMatcher;
 import org.apache.rat.analysis.IHeaders;
+import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.Log;
 import org.apache.rat.utils.ReportingSet;
 
@@ -121,6 +126,31 @@ public class LicenseSetFactory {
         licenses.forEach(l -> families.addIfNotPresent(l.getLicenseFamily()));
     }
 
+    public void validate() {
+        Log log = DefaultLog.getInstance();
+
+        // verify license definitions exist
+        if (getLicenses(LicenseFilter.ALL).isEmpty()) {
+            String msg = "At least one license must be defined";
+            log.error(msg);
+            throw new ConfigurationException(msg);
+        }
+
+        // verify that all approved license families exist
+        Set<String> exists = getLicenseFamilies(LicenseFilter.ALL)
+                .stream().map(ILicenseFamily::getFamilyCategory).collect(Collectors.toSet());
+        Set<String> approved = new HashSet<>(approvedLicenseCategories);
+        approved.removeIf(exists::contains);
+        approved.forEach(name -> log.warn(String.format("License category '%s' was approved but does not exist.", name)));
+
+        // verify that all approved licenses exist
+        exists = getLicenses(LicenseFilter.ALL)
+                .stream().map(ILicense::getId).collect(Collectors.toSet());
+        approved = new HashSet<>(approvedLicenseIds);
+        approved.removeIf(exists::contains);
+        approved.forEach(name -> log.warn(String.format("License '%s' was approved but does not exist.", name)));
+    }
+
     public void add(final LicenseSetFactory other) {
         this.families.addAll(other.families);
         this.licenses.addAll(other.licenses);
diff --git a/apache-rat-core/src/main/java/org/apache/rat/report/IReportable.java b/apache-rat-core/src/main/java/org/apache/rat/report/IReportable.java
index 2c108af6..a92ba059 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/report/IReportable.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/report/IReportable.java
@@ -33,5 +33,5 @@ public interface IReportable {
      * Returns the DocumentName for the reportable.
      * @return the DocumentName for the reportable.
      */
-    DocumentName getName();
+    DocumentName name();
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java b/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java
index 62572916..d47f2e5b 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/report/claim/ClaimStatistic.java
@@ -19,14 +19,26 @@
 
 package org.apache.rat.report.claim;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.commons.io.function.IOSupplier;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rat.ConfigurationException;
 import org.apache.rat.api.Document;
+import org.apache.rat.configuration.XMLConfigurationReader;
+import org.apache.rat.report.xml.writer.XmlWriter;
+import org.xml.sax.SAXException;
 
 /**
  * This class provides a numerical overview about
@@ -113,6 +125,9 @@ public class ClaimStatistic {
     /** Map of counter type to value */
     private final ConcurrentHashMap<ClaimStatistic.Counter, IntCounter> counterMap = new ConcurrentHashMap<>();
 
+    public Serde serde() {
+        return new Serde();
+    }
     /**
      * Converts null counter to 0.
      *
@@ -141,6 +156,15 @@ public class ClaimStatistic {
         counterMap.compute(counter, (k, v) -> v == null ? new IntCounter().increment(value) : v.increment(value));
     }
 
+    /**
+     * Increments the counts for the counter.
+     * @param counter the counter to increment.
+     * @param value the value to increment the counter by.
+     */
+    public void setCounter(final Counter counter, final int value) {
+        counterMap.put(counter, new IntCounter().increment(value));
+    }
+
     /**
      * Gets the counts for the Document.Type.
      * @param documentType the Document.Type to get the counter for.
@@ -288,5 +312,96 @@ public class ClaimStatistic {
         public int value() {
             return value;
         }
+
+        @Override
+        public String toString() {
+            return String.valueOf(value);
+        }
+    }
+
+    /**
+     * Serialze and deserialze the claim Statistic.
+     */
+    public class Serde {
+
+        public void serialize(final Appendable appendable) throws IOException {
+            try (XmlWriter writer = new XmlWriter(appendable)) {
+                writer.startDocument().openElement("ClaimStatistic")
+                        .openElement("licenseNameMap");
+                for (Map.Entry<String, IntCounter> entry : licenseNameMap.entrySet()) {
+                    if (entry.getValue().value > 0) {
+                        writer.openElement("licenseName")
+                                .attribute("count", entry.getValue().toString())
+                                .attribute("name", entry.getKey()).closeElement();
+                    }
+                }
+                writer.closeElement()
+                        .openElement("licenseFamilyCategoryMap");
+                for (Map.Entry<String, IntCounter> entry : licenseFamilyCategoryMap.entrySet()) {
+                    if (entry.getValue().value > 0) {
+                        writer.openElement("familyCategory")
+                                .attribute("count", entry.getValue().toString())
+                                .attribute("name", entry.getKey()).closeElement();
+                    }
+                }
+                writer.closeElement()
+                        .openElement("documentTypeMap");
+                for (Map.Entry<Document.Type, IntCounter> entry : documentTypeMap.entrySet()) {
+                    if (entry.getValue().value > 0) {
+                        writer.openElement("documentType")
+                                .attribute("count", entry.getValue().toString())
+                                .attribute("name", entry.getKey().name()).closeElement();
+                    }
+                }
+                writer.closeElement()
+                        .openElement("counterMap");
+                for (Map.Entry<ClaimStatistic.Counter, IntCounter> entry : counterMap.entrySet()) {
+                    if (entry.getValue().value > 0) {
+                        writer.openElement("counter")
+                                .attribute("count", entry.getValue().toString())
+                                .attribute("name", entry.getKey().name()).closeElement();
+                    }
+                }
+                writer.closeElement();
+            }
+        }
+
+        public void deserialize(final IOSupplier<InputStream> inputStreamSupplier) throws IOException {
+            DocumentBuilder builder;
+            try {
+                builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+            } catch (ParserConfigurationException e) {
+                throw new ConfigurationException("Unable to create DOM builder", e);
+            }
+            org.w3c.dom.Document document;
+            try (InputStream stream = inputStreamSupplier.get()) {
+                document = builder.parse(stream);
+
+            } catch (SAXException e) {
+                throw new IOException("Unable to read input", e);
+            }
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("licenseName"), node -> {
+                Map<String, String> attributes = XMLConfigurationReader.attributes(node);
+                incLicenseNameCount(attributes.get("name"), Integer.parseInt(attributes.get("count")));
+            });
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("familyCategory"), node -> {
+                Map<String, String> attributes = XMLConfigurationReader.attributes(node);
+                incLicenseCategoryCount(attributes.get("name"), Integer.parseInt(attributes.get("count")));
+            });
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("documentType"), node -> {
+                Map<String, String> attributes = XMLConfigurationReader.attributes(node);
+                Document.Type type = Document.Type.valueOf(attributes.get("name"));
+                incCounter(type, Integer.parseInt(attributes.get("count")));
+            });
+
+            XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("counter"), node -> {
+                Map<String, String> attributes = XMLConfigurationReader.attributes(node);
+                Counter type = Counter.valueOf(attributes.get("name"));
+                setCounter(type, Integer.parseInt(attributes.get("count")));
+            });
+        }
     }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/walker/FileListWalker.java b/apache-rat-core/src/main/java/org/apache/rat/walker/FileListWalker.java
index 05609085..1898c4e8 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/walker/FileListWalker.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/walker/FileListWalker.java
@@ -72,7 +72,7 @@ public class FileListWalker implements IReportable {
     @Override
     public void run(final RatReport report) throws RatException {
         DefaultLog.getInstance().debug(String.format("Reading file name: %s due to option %s", source, Arg.SOURCE.option()));
-        DocumentName sourceName = getName();
+        DocumentName sourceName = name();
         try (Reader reader = source.reader()) {
             for (String docName : IOUtils.readLines(reader)) {
                 try {
@@ -93,7 +93,7 @@ public class FileListWalker implements IReportable {
     }
 
     @Override
-    public DocumentName getName() {
+    public DocumentName name() {
         return source.getName();
     }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/walker/IReportableListWalker.java b/apache-rat-core/src/main/java/org/apache/rat/walker/IReportableListWalker.java
index b028b9c6..8df62905 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/walker/IReportableListWalker.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/walker/IReportableListWalker.java
@@ -61,13 +61,13 @@ public final class IReportableListWalker implements IReportable {
             try {
                 reportable.run(report);
             } catch (RatException e) {
-                DefaultLog.getInstance().error("Error processing " + reportable.getName(), e);
+                DefaultLog.getInstance().error("Error processing " + reportable.name(), e);
             }
         }
     }
 
     @Override
-    public DocumentName getName() {
+    public DocumentName name() {
         return documentName;
     }
 
diff --git a/apache-rat-core/src/main/java/org/apache/rat/walker/Walker.java b/apache-rat-core/src/main/java/org/apache/rat/walker/Walker.java
index 5651f7ab..f0e6d7bd 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/walker/Walker.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/walker/Walker.java
@@ -48,7 +48,7 @@ public abstract class Walker implements IReportable {
     }
 
     @Override
-    public DocumentName getName() {
+    public DocumentName name() {
         return document.getName();
     }
 }
diff --git a/apache-rat-core/src/main/resources/org/apache/rat/xhtml5.xsl b/apache-rat-core/src/main/resources/org/apache/rat/xhtml5.xsl
new file mode 100644
index 00000000..b1522460
--- /dev/null
+++ b/apache-rat-core/src/main/resources/org/apache/rat/xhtml5.xsl
@@ -0,0 +1,354 @@
+<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
+
+    <!--***********************************************************
+     *
+     * Licensed to the Apache Software Foundation (ASF) under one
+     * or more contributor license agreements.  See the NOTICE file
+     * distributed with this work for additional information
+     * regarding copyright ownership.  The ASF licenses this file
+     * to you under the Apache License, Version 2.0 (the
+     * "License"); you may not use this file except in compliance
+     * with the License.  You may obtain a copy of the License at
+     *
+     *   http://www.apache.org/licenses/LICENSE-2.0
+     *
+     * Unless required by applicable law or agreed to in writing,
+     * software distributed under the License is distributed on an
+     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+     * KIND, either express or implied.  See the License for the
+     * specific language governing permissions and limitations
+     * under the License.
+     *
+     ***********************************************************-->
+
+    <!-- This style sheet converts any rat-report.xml file.  -->
+
+    <xsl:output method='xml'/>
+
+    <xsl:variable name='notapproved'>&#10071;</xsl:variable>
+    <xsl:variable name="archive"><span style="font-size:x-large;">&#128476;</span></xsl:variable>
+    <xsl:variable name="binary"><span style="padding-left:4px;">&#128288;</span></xsl:variable>
+    <xsl:variable name="ignored"><span style="padding-left:4px;">&#128683;</span></xsl:variable>
+    <xsl:variable name="notice"><span style="font-size:x-large;padding-left:4px;">&#8505;</span></xsl:variable>
+    <xsl:variable name="standard"><span style="padding-left:4px;">&#9989;</span></xsl:variable>
+    <xsl:variable name="unknown">&#10067;</xsl:variable>
+    <xsl:variable name="directory"><span style="padding-left:4px;background-color: #00000087;">&#128194;</span></xsl:variable>
+
+    <xsl:template match="/">
+        <html>
+            <head>
+                <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+                <style type="text/css">
+                    <xsl:comment>
+                        body {margin-top: 0px;background-color: #F9F7ED;}
+
+                        h1 {color:red;}
+                        h2 {color:blue;}
+                        h3 {color:blueviolet;}
+                        h4 {color:green;}
+                        div.section {padding-left:5em;}
+                        div.subsection {padding-left:5em;}
+
+                        /* Table Design */
+                        table,tr,td {font-weight:bold;border:1px solid #000;}
+                        table {min-width: 50%;}
+                        caption {color:blue;text-align:left;}
+
+                        .document-name {font-size:larger;}
+                        .document-mediatype {float: left;}
+                        .document-encoding {padding-left: 2em;}
+                        .license-entry {padding-left: 1em;}
+                        .license-id {padding-left: 2em;}
+                        .license-family {padding-left: 1em;}
+                    </xsl:comment>
+                </style>
+            </head>
+            <body>
+                <xsl:apply-templates />
+            </body>
+        </html>
+    </xsl:template>
+
+    <xsl:template match="rat-report">
+
+        <h1>Rat Report</h1>
+
+        <xsl:call-template name="summary" />
+
+
+        <xsl:if test="descendant::resource[license/@approval='false']">
+            <xsl:call-template name="unapproved-files" />
+        </xsl:if>
+
+        <xsl:if test="descendant::resource[@type='ARCHIVE']">
+            <xsl:call-template name="archives" />
+        </xsl:if>
+
+        <h2>Detail</h2>
+
+        <p>
+            Documents with unapproved licenses will start with a <xsl:value-of select="$notapproved"/>
+            The first character on the next line identifies the document type.
+        </p>
+        <table id="rat-reports symbols" cellspacing="0"
+        summary="Symbols used in this RAT report">
+        <caption>
+            Symbols used in this RAT report
+        </caption>
+            <tr>
+                <th>Symbol</th>
+                <th>Type</th>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$archive"/></td>
+                <td>Archive file</td>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$binary"/></td>
+                <td>Binary file</td>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$ignored"/></td>
+                <td>Ignored file</td>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$notice"/></td>
+                <td>Notice file</td>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$standard"/></td>
+                <td>Standard file</td>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$unknown"/></td>
+                <td>Unknown file</td>
+            </tr>
+            <tr>
+                <td><xsl:value-of select="$directory"/></td>
+                <td>Directory</td>
+            </tr>
+        </table>
+
+        <table id="rat-reports resources" cellspacing="0"
+               summary="Resources discovered in this RAT report">
+            <caption>
+                Resources discovered in this RAT report
+            </caption>
+            <xsl:for-each select='descendant::resource'>
+                <tr>
+                    <td>
+                        <xsl:if test="@isDirectory = 'true'"><xsl:value-of select="$directory"/></xsl:if>
+                        <xsl:if test="license/@approval = 'false'"><xsl:value-of select="$notapproved"/></xsl:if>
+                        <xsl:choose>
+                            <xsl:when test="substring(@type, 1, 1) = 'A'"><xsl:value-of select="$archive"/></xsl:when>
+                            <xsl:when test="substring(@type, 1, 1) = 'B'"><xsl:value-of select="$binary"/></xsl:when>
+                            <xsl:when test="substring(@type, 1, 1) = 'I'"><xsl:value-of select="$ignored"/></xsl:when>
+                            <xsl:when test="substring(@type, 1, 1) = 'N'"><xsl:value-of select="$notice"/></xsl:when>
+                            <xsl:when test="substring(@type, 1, 1) = 'S'"><xsl:value-of select="$standard"/></xsl:when>
+                            <xsl:when test="substring(@type, 1, 1) = 'U'"><xsl:value-of select="$unknown"/></xsl:when>
+                        </xsl:choose>
+                    </td>
+                    <td>
+                        <div class="document-entry">
+                            <span class="document-name"><xsl:value-of select='@name'/></span>
+                            <div class="document-meta">
+                                <span class="document-mediatype"><xsl:value-of select="@mediaType"/></span>
+                                <span class="document-encoding"><xsl:value-of select="@encoding"/></span>
+                            </div>
+                        </div>
+                        <xsl:for-each select='descendant::license'>
+                            <div class="license-entry">
+                                <div>
+                                    <xsl:if test="@approval = 'false'">
+                                        <span class='approval-icon'><xsl:value-of select="$notapproved"/></span>
+                                    </xsl:if>
+                                    <span class="license-name"><xsl:value-of select="@name"/></span>
+                                </div>
+                                <span class="license-id">ID:
+                                    <xsl:choose>
+                                        <xsl:when test="@id = '?????'">UUID</xsl:when>
+                                        <xsl:otherwise><xsl:value-of select="@id"/></xsl:otherwise>
+                                    </xsl:choose>
+                                </span>
+                                <span class="license-family">Family:
+                                    <xsl:choose>
+                                        <xsl:when test="@family = '?????'">Unknown</xsl:when>
+                                        <xsl:otherwise><xsl:value-of select="@family"/></xsl:otherwise>
+                                    </xsl:choose>
+                                </span>
+                            </div>
+                        </xsl:for-each>
+                    </td>
+                </tr>
+            </xsl:for-each>
+        </table>
+    </xsl:template>
+
+    <xsl:template name="summary">
+        <h2>Summary</h2>
+
+        <div class="section">
+            <p>Generated at <xsl:value-of select='@timestamp' />  by
+                <xsl:value-of select='concat(version/@product, " ", rat-report/version/@version, " (", rat-report/version/@vendor, ")")'/>
+            </p>
+
+            <table id="rat-reports summary statistics" cellspacing="0"
+                   summary="A  summary of statistics from this RAT report">
+                <caption>
+                    Table 1: A summary of statistics from this RAT report.
+                </caption>
+                <tr>
+                    <th colspan="1">Name:</th>
+                    <th colspan="1">Count:</th>
+                    <th colspan="1">Description:</th>
+                </tr>
+                <xsl:for-each select='descendant::statistic'>
+                    <tr>
+                       <td>
+                            <xsl:if test='@approval="false"'>
+                                <span class='approval-icon'><xsl:value-of select="$notapproved"/></span>
+                            </xsl:if>
+                            <xsl:choose>
+                                <xsl:when test="@name = '?????'">Unknown</xsl:when>
+                                <xsl:otherwise><xsl:value-of select="@name"/></xsl:otherwise>
+                            </xsl:choose>
+                        </td>
+                        <td><xsl:value-of select='@count'/></td>
+                        <td><xsl:value-of select='@description'/></td>
+                    </tr>
+                </xsl:for-each>
+            </table>
+
+            <h3>License Statistics</h3>
+            <div class="subsection">
+                <h4>Categories</h4>
+                <div class="subsection">
+                    <table id="rat-reports summary license-categories" cellspacing="0"
+                           summary="A summary of license categories found in this RAT report">
+                        <caption>
+                            Table 2: License categories found in this RAT report.
+                        </caption>
+                        <tr>
+                            <th colspan="1">Name:</th>
+                            <th colspan="1">Count:</th>
+                        </tr>
+
+                        <xsl:for-each select='descendant::licenseCategory'>
+                            <tr>
+                                <td>
+                                    <xsl:if test='@approved="false"'>
+                                        <span class='approval-icon'><xsl:value-of select="$notapproved"/></span>
+                                    </xsl:if>
+                                    <xsl:choose>
+                                        <xsl:when test="@name = '?????'">Unknown</xsl:when>
+                                        <xsl:otherwise><xsl:value-of select="@name"/></xsl:otherwise>
+                                    </xsl:choose>
+                                </td>
+                                <td><xsl:value-of select='@count'/></td>
+                            </tr>
+                        </xsl:for-each>
+                    </table>
+                </div>
+
+                <h4>Licenses</h4>
+                <div class="subsection">
+                    <table id="rat-reports summary licenses" cellspacing="0"
+                           summary="Licenses found in this RAT report">
+                        <caption>
+                            Table 3: Licenses found in this RAT report.
+                        </caption>
+                        <tr>
+                            <td colspan="1">Name:</td>
+                            <td colspan="1">Count:</td>
+                        </tr>
+
+                        <xsl:for-each select='descendant::licenseName'>
+                            <tr>
+                                <td>
+                                    <xsl:if test='@approved="false"'>
+                                        <span class='approval-icon'><xsl:value-of select="$notapproved"/></span>
+                                    </xsl:if>
+                                    <xsl:choose>
+                                        <xsl:when test="@name = '?????'">Unknown</xsl:when>
+                                        <xsl:otherwise><xsl:value-of select="@name"/></xsl:otherwise>
+                                    </xsl:choose>
+                                </td>
+                                <td><xsl:value-of select='@count'/></td>
+                            </tr>
+                        </xsl:for-each>
+                    </table>
+                </div>
+            </div>
+
+            <h3>Document types</h3>
+            <div class="subsection">
+                <table id="rat-reports summary document-types" cellspacing="0"
+                       summary="Document types found in this RAT report">
+                    <caption>
+                        Table 4: Document types found in this RAT report.
+                    </caption>
+                    <tr>
+                        <th colspan="1">Name:</th>
+                        <th colspan="1">Count:</th>
+                    </tr>
+
+                    <xsl:for-each select='descendant::documentType'>
+                        <tr>
+                            <td><xsl:value-of select="@name"/></td>
+                            <td><xsl:value-of select="@count"/></td>
+                        </tr>
+                    </xsl:for-each>
+                </table>
+            </div>
+        </div>
+    </xsl:template>
+
+    <xsl:template name="unapproved-files">
+        <h2>Files with unapproved licenses</h2>
+
+        <ul class="unapproved-licenses">
+            <xsl:for-each select='descendant::resource[license/@approval="false"]'>
+                <li class="document-name"><xsl:value-of select='@name'/></li>
+            </xsl:for-each>
+        </ul>
+    </xsl:template>
+
+    <xsl:template name="archives">
+        <h2>Archives</h2>
+        <ul>
+            <xsl:for-each select='descendant::resource[@type="ARCHIVE"]'>
+                <li class="document-name"><xsl:value-of select='@name'/></li>
+            </xsl:for-each>
+        </ul>
+    </xsl:template>
+
+
+    <xsl:template name="license">
+        <xsl:param name="family"/>
+        <xsl:param name="id"/>
+        <xsl:param name="name"/>
+        <xsl:param name="approval"/>
+
+        <tr>
+            <td> </td>
+            <td>
+                <table>
+                    <tr>
+                        <xsl:if test="$approval = 'false'">
+                            <xsl:value-of select="$notapproved"/>
+                        </xsl:if>
+                    </tr>
+                    <tr>
+                        <div class="document-license">
+                            <span class="license-name"><xsl:value-of select="$name"/></span>
+                            <div class="documment-license-meta">
+                                <span class="id"><xsl:value-of select="$id"/></span>
+                                <span class="family"><xsl:value-of select="$family"/></span>
+                            </div>
+                        </div>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </xsl:template>
+</xsl:transform>
diff --git a/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java b/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java
index 0b011325..3172f3c0 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/OptionCollectionTest.java
@@ -225,7 +225,7 @@ public class OptionCollectionTest {
         ReportConfiguration config = OptionCollection.parseCommands(testPath.toFile(), new String[]{fName}, o -> fail("Help called"), false);
         IReportable reportable = OptionCollection.getReportable(base, config);
         assertThat(reportable).as(() -> format("'%s' returned null", fName)).isNotNull();
-        assertThat(reportable.getName().getName()).isEqualTo(expected);
+        assertThat(reportable.name().getName()).isEqualTo(expected);
     }
 
     @Test
diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java b/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java
index 69af88b2..f29c622e 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/ReportConfigurationTest.java
@@ -39,6 +39,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.SortedSet;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import org.apache.commons.io.filefilter.DirectoryFileFilter;
 import org.apache.commons.io.output.CloseShieldOutputStream;
@@ -56,6 +57,7 @@ import org.apache.rat.testhelpers.TestingLog;
 import org.apache.rat.testhelpers.TestingLicense;
 import org.apache.rat.testhelpers.TestingMatcher;
 import org.apache.rat.utils.DefaultLog;
+import org.apache.rat.utils.Log;
 import org.apache.rat.utils.Log.Level;
 import org.apache.rat.utils.ReportingSet.Options;
 import org.junit.jupiter.api.AfterEach;
@@ -456,7 +458,7 @@ public class ReportConfigurationTest {
         assertThat(underTest.getWriter()).isNotNull();
 
         ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        underTest.setOut(() -> stream);
+        underTest.setOut(new ReportConfiguration.IODescriptor<>("outputTest", () -> stream));
         assertThat(underTest.getOutput().get()).isEqualTo(stream);
         PrintWriter writer = underTest.getWriter().get();
         assertThat(writer).isNotNull();
@@ -480,9 +482,9 @@ public class ReportConfigurationTest {
         URL url = this.getClass().getResource("ReportConfigurationTestFile");
         assertThat(url).isNotNull();
 
-        assertThat(underTest.getStyleSheet()).isNull();
+        assertThat(underTest.getStyleSheetDescriptor()).isNull();
         InputStream stream = mock(InputStream.class);
-        underTest.setStyleSheet(() -> stream);
+        underTest.setStyleSheet(new ReportConfiguration.IODescriptor<>("stylesheetTest", () -> stream));
         assertThat(underTest.getStyleSheet().get()).isEqualTo(stream);
 
         File file = mock(File.class);
@@ -515,32 +517,37 @@ public class ReportConfigurationTest {
 
     @Test
     public void testValidate() {
-        final StringBuilder sb = new StringBuilder();
+        TestingLog testLog = new TestingLog();
+        Log oldLog = DefaultLog.getInstance();
+        try {
+            DefaultLog.setInstance(testLog);
+
         String msg = "At least one source must be specified";
-        assertThatThrownBy(() -> underTest.validate(sb::append)).isExactlyInstanceOf(ConfigurationException.class)
+            assertThatThrownBy(underTest::validate).isExactlyInstanceOf(ConfigurationException.class)
                 .hasMessageContaining(msg);
-        assertThat(sb.toString()).isEqualTo(msg);
+            testLog.assertContains(msg);
+            testLog.clear();
 
 
-        sb.setLength(0);
-        msg = "You must specify at least one license";
+            msg = "At least one license must be defined";
         underTest.addSource(mock(IReportable.class));
 
-        assertThatThrownBy(() -> underTest.validate(sb::append)).isExactlyInstanceOf(ConfigurationException.class)
+            assertThatThrownBy(underTest::validate).isExactlyInstanceOf(ConfigurationException.class)
                 .hasMessageContaining(msg);
-        assertThat(sb.toString()).isEqualTo(msg);
+            testLog.assertContains(msg);
 
-        sb.setLength(0);
         underTest.addLicense(testingLicense("valid", "Validation testing license"));
-        underTest.validate(sb::append);
-        assertThat(sb.length()).isEqualTo(0);
+            underTest.validate();
+        } finally {
+            DefaultLog.setInstance(oldLog);
+        }
     }
     
     @Test
     public void testSetOut() throws IOException {
         ReportConfiguration config = new ReportConfiguration();
         try (OutputStreamInterceptor osi = new OutputStreamInterceptor()) {
-            config.setOut(() -> osi);
+            config.setOut(new ReportConfiguration.IODescriptor<>("testSetOut", () -> osi));
             assertThat(osi.closeCount).isEqualTo(0);
             try (OutputStream os = config.getOutput().get()) {
                 assertThat(os).isNotNull();
@@ -563,16 +570,18 @@ public class ReportConfigurationTest {
        
         // verify default collision logs WARNING
         underTest.addFamily(ILicenseFamily.builder().setLicenseFamilyCategory("CAT").setLicenseFamilyName("name2"));
-        assertThat(log.getCaptured().contains("WARN")).as("default value not WARN").isTrue();
-        assertThat(log.getCaptured().contains("CAT")).as("'CAT' not found").isTrue();
+        assertThat(log.getCaptured()).contains("WARN").contains("CAT");
         
         // verify level setting works.
         for (Level l : Level.values()) {
           log.clear();
           underTest.logFamilyCollisions(l);
           underTest.addFamily(ILicenseFamily.builder().setLicenseFamilyCategory("CAT").setLicenseFamilyName("name2"));
-          assertThat(log.getCaptured().contains("CAT")).as("'CAT' not found").isTrue();
-          assertThat(log.getCaptured().contains(l.name())).as("logging not set to "+l).isTrue();
+          if (DefaultLog.getInstance().isEnabled(l)) {
+              assertThat(log.getCaptured()).contains("CAT").contains(l.name());
+          } else {
+              assertThat(log.getCaptured()).doesNotContain("CAT").doesNotContain(l.name());
+          }
         }
     }
     
@@ -662,51 +671,52 @@ public class ReportConfigurationTest {
                 .isExactlyInstanceOf(IllegalArgumentException.class);
     }
 
-    /**
-     * Validates that the configuration contains the default approved licenses.
-     * @param config The configuration to test.
-     */
-    public static void validateDefaultApprovedLicenses(ReportConfiguration config) {
-        validateDefaultApprovedLicenses(config, 0);
-    }
     
     /**
      * Validates that the configuration contains the default approved licenses.
      * @param config The configuration to test.
      */
-    public static void validateDefaultApprovedLicenses(ReportConfiguration config, int additionalIdCount) {
-        assertThat(config.getLicenseCategories(LicenseFilter.APPROVED)).hasSize(XMLConfigurationReaderTest.APPROVED_IDS.length + additionalIdCount);
-        for (String s : XMLConfigurationReaderTest.APPROVED_IDS) {
-            assertThat(config.getLicenseCategories(LicenseFilter.APPROVED)).contains(ILicenseFamily.makeCategory(s));
+    public static void validateDefaultApprovedLicenses(ReportConfiguration config, String... additionalIds) {
+        validateLicenses(config, Arrays.asList(additionalIds), LicenseFilter.APPROVED, XMLConfigurationReaderTest.APPROVED_LICENSES);
         }
+
+    /**
+     * Validates that the configuration contains all the default licenses along with any addiitonal licenses
+     * @param config the configuration to test.
+     * @param additionalLicenses Additional licence IDs that are expected.
+     */
+    public static void validateDefaultLicenses(ReportConfiguration config, String...additionalLicenses) {
+        validateLicenses(config, Arrays.asList(additionalLicenses), LicenseFilter.ALL, XMLConfigurationReaderTest.EXPECTED_LICENSES);
     }
 
+    private static void validateLicenses(ReportConfiguration config, List<String> additionalIds, LicenseFilter filter, String[] approvedIds) {
+        List<String> expected = new ArrayList<>(Arrays.asList(approvedIds));
+        expected.addAll(additionalIds);
+        assertThat(config.getLicenses(filter).stream().map(ILicense::getId).collect(Collectors.toSet())).containsExactlyInAnyOrderElementsOf(expected);
+    }
+
+
     /**
      * Validates that the configuration contains the default license families.
      * @param config the configuration to test.
      */
     public static void validateDefaultLicenseFamilies(ReportConfiguration config, String...additionalIds) {
-        assertThat(config.getLicenseFamilies(LicenseFilter.ALL)).hasSize(XMLConfigurationReaderTest.EXPECTED_IDS.length + additionalIds.length);
-        List<String> expected = new ArrayList<>();
-        expected.addAll(Arrays.asList(XMLConfigurationReaderTest.EXPECTED_IDS));
-        expected.addAll(Arrays.asList(additionalIds));
-        for (ILicenseFamily family : config.getLicenseFamilies(LicenseFilter.ALL)) {
-            assertThat(expected).contains(family.getFamilyCategory().trim());
-        }
+        validateLicenseFamilies(config, Arrays.asList(additionalIds), LicenseFilter.ALL, XMLConfigurationReaderTest.EXPECTED_IDS);
     }
 
     /**
-     * Validates that the configuration contains the default licenses.
+     * Validates that the configuration contains the default license families.
      * @param config the configuration to test.
      */
-    public static void validateDefaultLicenses(ReportConfiguration config, String...additionalLicenses) {
-        assertThat(config.getLicenses(LicenseFilter.ALL)).hasSize(XMLConfigurationReaderTest.EXPECTED_LICENSES.length + additionalLicenses.length);
-        List<String> expected = new ArrayList<>();
-        expected.addAll(Arrays.asList(XMLConfigurationReaderTest.EXPECTED_LICENSES));
-        expected.addAll(Arrays.asList(additionalLicenses));
-        for (ILicense license : config.getLicenses(LicenseFilter.ALL)) {
-            assertThat(expected).contains(license.getId());
+    public static void validateDefaultApprovedLicenseFamilies(ReportConfiguration config, String...additionalIds) {
+        validateLicenseFamilies(config, Arrays.asList(additionalIds), LicenseFilter.APPROVED, XMLConfigurationReaderTest.APPROVED_IDS);
         }
+
+    private static void validateLicenseFamilies(ReportConfiguration config, List<String> additionalIds, LicenseFilter filter, String[] approvedIds) {
+        List<String> expected = new ArrayList<>(Arrays.asList(approvedIds));
+        expected.addAll(additionalIds);
+        assertThat(config.getLicenseFamilies(filter).stream().map(lf -> lf.getFamilyCategory().trim())
+                .collect(Collectors.toSet())).containsExactlyInAnyOrderElementsOf(expected);
     }
     
     /**
@@ -719,9 +729,10 @@ public class ReportConfigurationTest {
         assertThat(config.getCopyrightMessage()).isNull();
         assertThat(config.getStyleSheet()).withFailMessage("Stylesheet should not be null").isNotNull();
 
-        validateDefaultApprovedLicenses(config);
         validateDefaultLicenseFamilies(config);
+        validateDefaultApprovedLicenseFamilies(config);
         validateDefaultLicenses(config);
+        validateDefaultApprovedLicenses(config);
     }
 
     /**
diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java
index 724b4919..ebec92b6 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsProvider.java
@@ -128,9 +128,9 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
         try {
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
+            Reporter.Output output = reporter.execute();
             ClaimValidator validator = config.getClaimValidator();
-            assertThat(validator.listIssues(claimStatistic)).isEmpty();
+            assertThat(validator.listIssues(output.getStatistic())).isEmpty();
         } finally {
             DefaultLog.setInstance(null);
         }
@@ -156,8 +156,8 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             FileUtils.delete(resultFile);
 
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic).isNotNull();
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic()).isNotNull();
             String contents = String.join("\n", IOUtils.readLines(new FileReader(testFile)));
             assertThat(contents).isEqualTo("class NoLicense {}");
             assertThat(resultFile).exists();
@@ -212,9 +212,9 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
+            Reporter.Output output = reporter.execute();
             ClaimValidator validator = config.getClaimValidator();
-            assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
+            assertThat(validator.listIssues(output.getStatistic())).containsExactly("UNAPPROVED");
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -246,9 +246,9 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 reporter.execute();
                 fail("Should have thrown exception");
             } catch (RatException e) {
-                ClaimStatistic claimStatistic = reporter.getClaimsStatistic();
+               // ClaimStatistic claimStatistic = reporter.getClaimsStatistic();
                 ClaimValidator validator = config.getClaimValidator();
-                assertThat(validator.listIssues(claimStatistic)).containsExactlyInAnyOrder("DOCUMENT_TYPES", "LICENSE_CATEGORIES", "LICENSE_NAMES", "STANDARDS");
+              //  assertThat(validator.listIssues(claimStatistic)).containsExactlyInAnyOrder("DOCUMENT_TYPES", "LICENSE_CATEGORIES", "LICENSE_NAMES", "STANDARDS");
             }
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
@@ -276,16 +276,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
+            Reporter.Output output = reporter.execute();
             ClaimValidator validator = config.getClaimValidator();
-            assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
+            assertThat(validator.listIssues(output.getStatistic())).containsExactly("UNAPPROVED");
 
             arg[0] = "Unapproved:1";
             config = generateConfig(ImmutablePair.of(option, arg));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
+            output  = reporter.execute();
             validator = config.getClaimValidator();
-            assertThat(validator.listIssues(claimStatistic)).isEmpty();
+            assertThat(validator.listIssues(output.getStatistic())).isEmpty();
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -303,16 +303,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
+            Reporter.Output output = reporter.execute();
             ClaimValidator validator = config.getClaimValidator();
-            assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
+            assertThat(validator.listIssues(output.getStatistic())).containsExactly("UNAPPROVED");
 
             arg[0] = "Unapproved:1";
             config = generateConfig(ImmutablePair.of(option, arg));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
+            output = reporter.execute();
             validator = config.getClaimValidator();
-            assertThat(validator.listIssues(claimStatistic)).isEmpty();
+            assertThat(validator.listIssues(output.getStatistic())).isEmpty();
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -332,16 +332,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
 
             // filter out source
             config = generateConfig(ImmutablePair.of(option, args));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(3);
+            output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(3);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -386,16 +386,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
 
             // filter out source
             config = generateConfig(ImmutablePair.of(option, args));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
+            output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -422,15 +422,15 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(4);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(5);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(4);
 
             config = generateConfig(ImmutablePair.of(option, args));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5);
+            output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -480,16 +480,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(11);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(11);
             // .gitignore is ignored by default as it is hidden but not counted
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
 
             config = generateConfig(ImmutablePair.of(option, args));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(8);
+            output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(8);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -509,25 +509,25 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.emptyList());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(4);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
 
             // verify exclude removes most files.
             config = generateConfig(ImmutablePair.of(excludeOption, EXCLUDE_ARGS));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
             // .gitignore is ignored by default as it is hidden but not counted
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(3);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(3);
 
             // verify include pust them back
             config = generateConfig(ImmutablePair.of(option, args), ImmutablePair.of(excludeOption, EXCLUDE_ARGS));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
             // .gitignore is ignored by default as it is hidden but not counted
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -579,15 +579,15 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(Collections.singletonList(excludes));
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(5);
 
             config = generateConfig(excludes, ImmutablePair.of(option, args));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(7);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(7);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(1);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -605,15 +605,15 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig();
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(3);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
 
             config = generateConfig(ImmutablePair.of(option, new String[]{inputFile.getAbsolutePath()}));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(0);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -636,18 +636,18 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = addCatzLicense(generateConfig());
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
+            Reporter.Output output = reporter.execute();
 
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
             config = addCatzLicense(generateConfig(arg1));
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -677,17 +677,17 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig();
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
 
             config = generateConfig(arg1);
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -721,26 +721,26 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig();
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
             config = generateConfig(arg1);
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
 
             Pair<Option, String[]> arg2 = ImmutablePair.of(Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"), null);
 
             config = generateConfig(arg1, arg2);
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
@@ -769,17 +769,17 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig();
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
             config = generateConfig(arg1);
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
 
 
         } catch (IOException | RatException e) {
@@ -816,17 +816,17 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig();
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
 
             config = generateConfig(arg1);
             reporter = new Reporter(config);
-            claimStatistic = reporter.execute();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            output  = reporter.execute();
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
         }
@@ -842,16 +842,17 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.output();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
+            Reporter.Output output = reporter.execute();
+            output.format(config);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(0);
 
             String actualText = TextUtils.readFile(outFile);
             TextUtils.assertContainsExactly(1, "Apache License 2.0: 1 ", actualText);
             TextUtils.assertContainsExactly(1, "STANDARD: 1 ", actualText);
 
-        } catch (IOException | RatException e) {
+        } catch (IOException | RatException e ) {
             fail(e.getMessage(), e);
         }
     }
@@ -885,10 +886,11 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 args[0] = sheet.arg();
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 Reporter reporter = new Reporter(config);
-                ClaimStatistic claimStatistic = reporter.output();
-                assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-                assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
-                assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+                Reporter.Output output  = reporter.execute();
+                output.format(config);
+                assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+                assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
+                assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
                 String actualText = baos.toString(StandardCharsets.UTF_8.name());
                 switch (sheet) {
@@ -906,6 +908,9 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                     case UNAPPROVED_LICENSES:
                         TextUtils.assertContainsExactly(1, "Files with unapproved licenses:" + System.lineSeparator() + "  /stylesheet", actualText);
                         break;
+                    case XHTML5:
+                        TextUtils.assertPatternInTarget("<td>Approved<\\/td>\\s+<td>\\d+<\\/td>\\s+<td>A count of approved licenses.<\\/td>", actualText);
+                        break;
                     default:
                         fail("No test for stylesheet " + sheet);
                         break;
@@ -915,10 +920,11 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
             args[0] = file.getAbsolutePath();
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.output();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            Reporter.Output output  = reporter.execute();
+            output.format(config);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
             String actualText = baos.toString(StandardCharsets.UTF_8.name());
             TextUtils.assertContainsExactly(1, "Hello world", actualText);
@@ -958,15 +964,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig(ImmutablePair.of(option, null));
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.output();
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
+            Reporter.Output output  = reporter.execute();
+            output.format(config);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(1);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.APPROVED)).isEqualTo(0);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)).isEqualTo(1);
 
             String actualText = baos.toString(StandardCharsets.UTF_8.name());
             TextUtils.assertContainsExactly(1, "<resource encoding=\"ISO-8859-1\" mediaType=\"text/plain\" name=\"/stylesheet\" type=\"STANDARD\">", actualText);
 
-            try (InputStream expected = StyleSheets.getStyleSheet("xml").get();
+            try (InputStream expected = StyleSheets.getStyleSheet("xml", null).ioSupplier().get();
                  InputStream actual = config.getStyleSheet().get()) {
                 assertThat(IOUtils.contentEquals(expected, actual)).as("'xml' does not match").isTrue();
             }
@@ -989,12 +996,12 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
             ReportConfiguration config = generateConfig();
             Reporter reporter = new Reporter(config);
-            reporter.output();
+            reporter.execute();
             TextUtils.assertNotContains("DEBUG", baos.toString(StandardCharsets.UTF_8.name()));
 
             config = generateConfig(ImmutablePair.of(option, new String[]{"debug"}));
             reporter = new Reporter(config);
-            reporter.output();
+            reporter.execute();
             TextUtils.assertContains("DEBUG", baos.toString(StandardCharsets.UTF_8.name()));
         } catch (IOException | RatException e) {
             fail(e.getMessage(), e);
@@ -1010,16 +1017,15 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
         try {
             configureSourceDir(option);
-            File outFile = new File(sourceDir, "out.xml");
-            FileUtils.delete(outFile);
-            ImmutablePair<Option, String[]> outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()});
-            ImmutablePair<Option, String[]> stylesheet = ImmutablePair.of(Arg.OUTPUT_STYLE.option(), new String[]{StyleSheets.XML.arg()});
+//            File outFile = new File(sourceDir, "out.xml");
+//            FileUtils.delete(outFile);
+//            ImmutablePair<Option, String[]> outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()});
+//            ImmutablePair<Option, String[]> stylesheet = ImmutablePair.of(Arg.OUTPUT_STYLE.option(), new String[]{StyleSheets.XML.arg()});
             for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
                 args[0] = filter.name();
-                ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args));
+                ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 Reporter reporter = new Reporter(config);
-                reporter.output();
-                Document document = XmlUtils.toDom(new FileInputStream(outFile));
+                Document document = reporter.execute().getDocument();
                 switch (filter) {
                     case ALL:
                         XmlUtils.assertIsPresent(filter.name(), document, xPath, "/rat-report/rat-config/licenses/license[@id='AL2.0']");
@@ -1037,8 +1043,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected filter: " + filter);
                 }
             }
-        } catch (IOException | RatException | SAXException | ParserConfigurationException |
-                 XPathExpressionException e) {
+        } catch (IOException | RatException | XPathExpressionException e) {
             fail(e.getMessage(), e);
         }
     }
@@ -1059,16 +1064,16 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
 
         try {
             configureSourceDir(option);
-            File outFile = new File(sourceDir, "out.xml");
-            FileUtils.delete(outFile);
-            ImmutablePair<Option, String[]> outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()});
-            ImmutablePair<Option, String[]> stylesheet = ImmutablePair.of(Arg.OUTPUT_STYLE.option(), new String[]{StyleSheets.XML.arg()});
+//            File outFile = new File(sourceDir, "out.xml");
+//            FileUtils.delete(outFile);
+//            ImmutablePair<Option, String[]> outputFile = ImmutablePair.of(Arg.OUTPUT_FILE.option(), new String[]{outFile.getAbsolutePath()});
+//            ImmutablePair<Option, String[]> stylesheet = ImmutablePair.of(Arg.OUTPUT_STYLE.option(), new String[]{StyleSheets.XML.arg()});
             for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
                 args[0] = filter.name();
-                ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args));
+                ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
                 Reporter reporter = new Reporter(config);
-                reporter.output();
-                Document document = XmlUtils.toDom(Files.newInputStream(outFile.toPath()));
+                Reporter.Output output = reporter.execute();
+                Document document = output.getDocument();
                 switch (filter) {
                     case ALL:
                         XmlUtils.assertIsPresent(filter.name(), document, xPath, "/rat-report/rat-config/families/family[@id='AL']");
@@ -1086,8 +1091,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected filter: " + filter);
                 }
             }
-        } catch (IOException | RatException | SAXException | ParserConfigurationException |
-                 XPathExpressionException e) {
+        } catch (IOException | RatException | XPathExpressionException e) {
             fail(e.getMessage(), e);
         }
     }
@@ -1123,9 +1127,8 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 args[0] = proc.name();
                 ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args));
                 Reporter reporter = new Reporter(config);
-                reporter.output();
 
-                Document document = XmlUtils.toDom(Files.newInputStream(outFile.toPath()));
+                Document document = reporter.execute().getDocument();
                 XmlUtils.assertIsPresent(proc.name(), document, xPath, "/rat-report/resource[@name='/dummy.jar']");
                 switch (proc) {
                     case ABSENCE:
@@ -1144,8 +1147,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected processing " + proc);
                 }
             }
-        } catch (IOException | RatException | SAXException | ParserConfigurationException |
-                 XPathExpressionException e) {
+        } catch (IOException | RatException | XPathExpressionException e) {
             fail(e.getMessage(), e);
         }
     }
@@ -1176,9 +1178,9 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                 args[0] = proc.name();
                 ReportConfiguration config = generateConfig(outputFile, stylesheet, ImmutablePair.of(option, args));
                 Reporter reporter = new Reporter(config);
-                reporter.output();
+                Reporter.Output output = reporter.execute();
 
-                Document document = XmlUtils.toDom(Files.newInputStream(outFile.toPath()));
+                Document document = output.getDocument();
                 XmlUtils.assertIsPresent(proc.name(), document, xPath, testDoc);
                 XmlUtils.assertIsPresent(proc.name(), document, xPath, missingDoc);
 
@@ -1199,8 +1201,7 @@ class ReporterOptionsProvider extends AbstractOptionsProvider implements Argumen
                         throw new IllegalArgumentException("Unexpected processing " + proc);
                 }
             }
-        } catch (IOException | RatException | SAXException | ParserConfigurationException |
-                 XPathExpressionException e) {
+        } catch (IOException | RatException | XPathExpressionException e) {
             fail(e.getMessage(), e);
         }
     }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java
index 2694b4c9..4f52a31a 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/ReporterOptionsTest.java
@@ -78,14 +78,14 @@ public final class ReporterOptionsTest {
             FileUtils.writeFile(testDir, "foo.md");
             ReportConfiguration config = OptionCollection.parseCommands(testDir, args, o -> fail("Help called"), true);
             Reporter reporter = new Reporter(config);
-            ClaimStatistic claimStatistic = reporter.execute();
-            XmlUtils.printDocument(System.out, reporter.getDocument());
+            Reporter.Output output = reporter.execute();
+            XmlUtils.printDocument(System.out, output.getDocument());
             XPath xpath = XPathFactory.newInstance().newXPath();
-            XmlUtils.assertIsPresent(reporter.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']");
-            XmlUtils.assertAttributes(reporter.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']",
+            XmlUtils.assertIsPresent(output.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']");
+            XmlUtils.assertAttributes(output.getDocument(), xpath, "/rat-report/resource[@name='/foo.md']",
                     XmlUtils.mapOf("type", "IGNORED"));
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(0);
-            assertThat(claimStatistic.getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(2);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.STANDARDS)).isEqualTo(0);
+            assertThat(output.getStatistic().getCounter(ClaimStatistic.Counter.IGNORED)).isEqualTo(2);
         } catch (IOException | RatException | XPathExpressionException e) {
             fail(e);
         }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java b/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java
index 3c2a9a72..d8f85e0d 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/ReporterTest.java
@@ -19,21 +19,24 @@
 package org.apache.rat;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Fail.fail;
 
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.IOException;
 import java.io.PrintStream;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.stream.Stream;
 import javax.xml.XMLConstants;
 import javax.xml.transform.Source;
 import javax.xml.transform.dom.DOMSource;
@@ -45,24 +48,29 @@ import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathConstants;
 import javax.xml.xpath.XPathFactory;
 
-import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.DefaultParser;
 import org.apache.commons.cli.ParseException;
 import org.apache.commons.io.FileUtils;
 import org.apache.rat.api.Document.Type;
 import org.apache.rat.api.RatException;
+import org.apache.rat.commandline.Arg;
 import org.apache.rat.commandline.ArgumentContext;
-import org.apache.rat.commandline.StyleSheets;
 import org.apache.rat.document.FileDocument;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.license.ILicenseFamily;
 import org.apache.rat.report.claim.ClaimStatistic;
 import org.apache.rat.test.utils.Resources;
+import org.apache.rat.testhelpers.BaseOptionCollection;
 import org.apache.rat.testhelpers.TextUtils;
 import org.apache.rat.testhelpers.XmlUtils;
+import org.apache.rat.testhelpers.data.ReportTestDataProvider;
+import org.apache.rat.testhelpers.data.TestData;
+import org.apache.rat.testhelpers.data.ValidatorData;
 import org.apache.rat.walker.DirectoryWalker;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.w3c.dom.Document;
 import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
@@ -74,19 +82,19 @@ public class ReporterTest {
     @TempDir
     File tempDirectory;
     final String basedir;
+    private final OptionCollectionParser collectionParser;
 
     ReporterTest() throws URISyntaxException {
         basedir = Resources.getExampleResource("exampleData").getPath();
+        collectionParser = new OptionCollectionParser(BaseOptionCollection.builder().build());
     }
 
     @Test
-    public void testExecute() throws RatException, ParseException {
+    public void testExecute() throws RatException, ParseException, IOException {
         File output = new File(tempDirectory, "testExecute");
-
-        CommandLine cl = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
-        ArgumentContext ctxt = new ArgumentContext(new File("."), cl);
-        ReportConfiguration config = OptionCollection.createConfiguration(ctxt);
-        ClaimStatistic statistic = new Reporter(config).execute();
+        BaseOptionCollection optionCollection = BaseOptionCollection.builder().build();
+        ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
+        ClaimStatistic statistic = new Reporter(ctxt.getConfiguration()).execute().getStatistic();
 
         assertThat(statistic.getCounter(Type.ARCHIVE)).isEqualTo(1);
         assertThat(statistic.getCounter(Type.BINARY)).isEqualTo(2);
@@ -137,11 +145,8 @@ public class ReporterTest {
     @Test
     public void testOutputOption() throws Exception {
         File output = new File(tempDirectory, "test");
-        CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"-o", output.getCanonicalPath(), basedir});
-        ArgumentContext ctxt = new ArgumentContext(new File("."), commandLine);
-
-        ReportConfiguration config = OptionCollection.createConfiguration(ctxt);
-        new Reporter(config).output();
+        ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-file", output.getCanonicalPath(), basedir});
+        new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration());
         assertThat(output.exists()).isTrue();
         String content = FileUtils.readFileToString(output, StandardCharsets.UTF_8);
         TextUtils.assertPatternInTarget("^! Unapproved:\\s*2 ", content);
@@ -152,15 +157,13 @@ public class ReporterTest {
     @Test
     public void testDefaultOutput() throws Exception {
         File output = new File(tempDirectory, "testDefaultOutput");
+        BaseOptionCollection optionCollection = BaseOptionCollection.builder().build();
 
         PrintStream origin = System.out;
         try (PrintStream out = new PrintStream(output)) {
             System.setOut(out);
-            CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{basedir});
-            ArgumentContext ctxt = new ArgumentContext(new File("."), commandLine);
-
-            ReportConfiguration config = OptionCollection.createConfiguration(ctxt);
-            new Reporter(config).output();
+            ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{basedir});
+            new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration());
         } finally {
             System.setOut(origin);
         }
@@ -209,11 +212,8 @@ public class ReporterTest {
                 "type", "STANDARD"));
 
         File output = new File(tempDirectory, "testXMLOutput");
-        CommandLine commandLine = new DefaultParser().parse(OptionCollection.buildOptions(), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
-        ArgumentContext ctxt = new ArgumentContext(new File("."), commandLine);
-
-        ReportConfiguration config = OptionCollection.createConfiguration(ctxt);
-        new Reporter(config).output();
+        ArgumentContext ctxt = collectionParser.parseCommands(new File("."), new String[]{"--output-style", "xml", "--output-file", output.getPath(), basedir});
+        new Reporter(ctxt.getConfiguration()).execute().format(ctxt.getConfiguration());
 
         assertThat(output).exists();
         Document doc = XmlUtils.toDom(java.nio.file.Files.newInputStream(output.toPath()));
@@ -403,13 +403,8 @@ public class ReporterTest {
 
     @Test
     public void xmlReportTest() throws Exception {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-
         ReportConfiguration configuration = initializeConfiguration();
-        configuration.setStyleSheet(StyleSheets.XML.getStyleSheet());
-        configuration.setOut(() -> out);
-        new Reporter(configuration).output();
-        Document doc = XmlUtils.toDom(new ByteArrayInputStream(out.toByteArray()));
+        Document doc = new Reporter(configuration).execute().getDocument();
 
         XPath xPath = XPathFactory.newInstance().newXPath();
 
@@ -455,10 +450,9 @@ public class ReporterTest {
                 "Generated at: ";
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ReportConfiguration configuration = initializeConfiguration();
-        configuration.setOut(() -> out);
-        new Reporter(configuration).output();
+        configuration.setOut(new ReportConfiguration.IODescriptor<>("plainReportTest", () -> out));
+        new Reporter(configuration).execute().format(configuration);
 
-        out.flush();
         String document = out.toString();
 
         TextUtils.assertNotContains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", document);
@@ -471,11 +465,10 @@ public class ReporterTest {
     public void unapprovedLicensesReportTest() throws Exception {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ReportConfiguration configuration = initializeConfiguration();
-        configuration.setOut(() -> out);
+        configuration.setOut(new ReportConfiguration.IODescriptor<>("unapprovedLicensesReportTest", () -> out));
         configuration.setStyleSheet(this.getClass().getResource("/org/apache/rat/unapproved-licenses.xsl"));
-        new Reporter(configuration).output();
+        new Reporter(configuration).execute().format(configuration);
 
-        out.flush();
         String document = out.toString();
 
         TextUtils.assertContains("Generated at: ", document );
@@ -486,21 +479,50 @@ public class ReporterTest {
     @Test
     public void counterMaxTest() throws Exception {
         ReportConfiguration config = initializeConfiguration();
-        Reporter reporter = new Reporter(config);
-        reporter.output();
+        Reporter.Output output = new Reporter(config).execute();
         assertThat(config.getClaimValidator().hasErrors()).isTrue();
-        assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, reporter.getClaimsStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)))
+        assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)))
                 .isFalse();
 
         config = initializeConfiguration();
         config.getClaimValidator().setMax(ClaimStatistic.Counter.UNAPPROVED, 2);
-        reporter = new Reporter(config);
-        reporter.output();
+        output = new Reporter(config).execute();
         assertThat(config.getClaimValidator().hasErrors()).isFalse();
-        assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, reporter.getClaimsStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)))
+        assertThat(config.getClaimValidator().isValid(ClaimStatistic.Counter.UNAPPROVED, output.getStatistic().getCounter(ClaimStatistic.Counter.UNAPPROVED)))
                 .isTrue();
     }
 
+    static Stream<Arguments> getTestData() {
+        BaseOptionCollection.Builder builder = BaseOptionCollection.builder()
+                        .unsupported(Arg.OUTPUT_FILE);
+        return new ReportTestDataProvider().getOptionTests(builder.build()).stream().map(testData ->
+                Arguments.of(testData.getTestName(), testData));
+    }
+
+    @ParameterizedTest( name = "{index} {0}")
+    @MethodSource("getTestData")
+    void testReportData(String name, TestData test) throws Exception {
+        Path tempPath = tempDirectory.toPath();
+        Path basePath = tempPath.resolve(test.getTestName());
+        org.apache.rat.utils.FileUtils.mkDir(basePath.toFile());
+        test.setupFiles(basePath);
+        ArgumentContext ctxt = collectionParser.parseCommands(basePath.toFile(),
+                test.getCommandLine(basePath.toString()));
+        if (test.expectingException()) {
+            assertThatThrownBy(() -> new Reporter(ctxt.getConfiguration()).execute()).as("Expected throws from " + name)
+                    .hasMessageContaining(test.getExpectedException().getMessage());
+            ValidatorData data = new ValidatorData(Reporter.Output.builder().configuration(ctxt.getConfiguration()).build(),
+                    basePath.toString());
+            test.getValidator().accept(data);
+        } else {
+            Reporter.Output output = ctxt.getConfiguration() != null ? new Reporter(ctxt.getConfiguration()).execute() :
+                    Reporter.Output.builder().build();
+            ValidatorData data = new ValidatorData(output, basePath.toString());
+            data.getOutput().format(data.getConfiguration());
+            test.getValidator().accept(data);
+        }
+    }
+
     private record LicenseInfo(String id, String family, boolean approval, boolean hasNotes) {
             LicenseInfo(String id, boolean approval, boolean hasNotes) {
                 this(id, id, approval, hasNotes);
diff --git a/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java b/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java
index a7863273..949dd1f1 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/configuration/XMLConfigurationReaderTest.java
@@ -29,6 +29,7 @@ import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.Collection;
 
+import java.util.stream.Collectors;
 import org.apache.rat.analysis.IHeaderMatcher;
 import org.apache.rat.config.parameters.ComponentType;
 import org.apache.rat.config.parameters.Description;
@@ -55,6 +56,10 @@ public class XMLConfigurationReaderTest {
     public static final String[] EXPECTED_LICENSES = { "AL1.0", "AL1.1", "AL2.0", "BSD-3", "DOJO", "TMF", "CDDL1", "ILLUMOS", "GPL1", "GPL2",
             "GPL3", "MIT", "OASIS", "W3C", "W3CD" };
 
+    public static final String[] APPROVED_LICENSES = { "AL1.0", "AL1.1", "AL2.0", "BSD-3", "DOJO", "TMF", "CDDL1", "ILLUMOS",
+            "MIT", "OASIS", "W3C", "W3CD" };
+
+
     @Test
     public void approvedLicenseIdTest() throws URISyntaxException {
         XMLConfigurationReader reader = new XMLConfigurationReader();
@@ -62,9 +67,8 @@ public class XMLConfigurationReaderTest {
         assertThat(url).isNotNull();
         reader.read(url.toURI());
 
-        Collection<String> readCategories = reader.approvedLicenseId();
-
-        assertArrayEquals(APPROVED_IDS, readCategories.toArray(new String[readCategories.size()]));
+        Collection<String> actual = reader.approvedLicenseId();
+        assertThat(actual).containsExactlyInAnyOrder(APPROVED_IDS);
     }
 
     @Test
@@ -80,9 +84,12 @@ public class XMLConfigurationReaderTest {
     public void LicenseFamiliesTest() throws URISyntaxException {
         XMLConfigurationReader reader = new XMLConfigurationReader();
         URL url = XMLConfigurationReaderTest.class.getResource("/org/apache/rat/default.xml");
+        assertNotNull(url);
         reader.read(url.toURI());
 
-        assertArrayEquals(EXPECTED_IDS, reader.readFamilies().stream().map(x -> x.getFamilyCategory().trim()).toArray(String[]::new));
+        Collection<String> actual = reader.readFamilies().stream().map(lf -> lf.getFamilyCategory().trim())
+                .collect(Collectors.toList());
+        assertThat(actual).containsExactlyInAnyOrder(EXPECTED_IDS);
     }
 
     private void checkMatcher(String name, Class<? extends AbstractBuilder> clazz) {
@@ -118,7 +125,7 @@ public class XMLConfigurationReaderTest {
 
         IHeaderMatcher.Builder builder = MatcherBuilderTracker.getMatcherBuilder("copyright");
         Description desc = DescriptionBuilder.buildMap(builder.getClass());
-        assertNotNull(desc, () -> "did not build description for 'copyright'");
+        assertNotNull(desc, "did not build description for 'copyright'");
         assertEquals("copyright", desc.getCommonName());
         assertEquals(ComponentType.MATCHER, desc.getType());
         assertFalse(desc.isCollection());
diff --git a/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java b/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java
index 6c4aa2ab..56de6e26 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/test/AbstractConfigurationOptionsProvider.java
@@ -790,7 +790,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
             for (String sheet : new String[]{"plain-rat", "missing-headers", "unapproved-licenses", file.getAbsolutePath()}) {
                 args[0] = sheet;
                 ReportConfiguration config = generateConfig(ImmutablePair.of(option, args));
-                try (InputStream expected = StyleSheets.getStyleSheet(sheet).get();
+                try (InputStream expected = StyleSheets.getStyleSheet(sheet, DocumentName.builder(baseDir).build()).ioSupplier().get();
                      InputStream actual = config.getStyleSheet().get()) {
                     assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", sheet)).isTrue();
                 }
@@ -821,7 +821,7 @@ public abstract class AbstractConfigurationOptionsProvider extends AbstractOptio
     protected void xmlTest() {
         try {
             ReportConfiguration config = generateConfig(ImmutablePair.of(Arg.OUTPUT_STYLE.find("xml"), null));
-            try (InputStream expected = StyleSheets.getStyleSheet("xml").get();
+            try (InputStream expected = StyleSheets.getStyleSheet("xml", null).ioSupplier().get();
                  InputStream actual = config.getStyleSheet().get()) {
                 assertThat(IOUtils.contentEquals(expected, actual)).as("'xml' does not match").isTrue();
             }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOption.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOption.java
new file mode 100644
index 00000000..c6ae379b
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOption.java
@@ -0,0 +1,25 @@
+
+package org.apache.rat.testhelpers;
+
+import org.apache.commons.cli.Option;
+import org.apache.rat.ui.ArgumentTracker;
+import org.apache.rat.ui.UIOption;
+import org.apache.rat.ui.UIOptionCollection;
+import org.apache.rat.utils.CasedString;
+
+public final class BaseOption extends UIOption<BaseOption> {
+    BaseOption(final UIOptionCollection<BaseOption> collection, Option option) {
+        super(collection, option, new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option)));
+    }
+    protected String cleanupName(Option option) {
+        return ArgumentTracker.extractKey(option);
+    }
+
+    public String getExample() {
+        return "";
+    }
+
+    public String getText() {
+        return "";
+    }
+}
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOptionCollection.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOptionCollection.java
new file mode 100644
index 00000000..615d8a09
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/BaseOptionCollection.java
@@ -0,0 +1,24 @@
+package org.apache.rat.testhelpers;
+
+import org.apache.commons.cli.Option;
+import org.apache.rat.ui.UIOptionCollection;
+
+public final class BaseOptionCollection extends UIOptionCollection<BaseOption> {
+    private BaseOptionCollection(Builder builder) {
+        super(builder);
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final class Builder extends UIOptionCollection.Builder<BaseOption, Builder> {
+        private Builder() {
+            super(BaseOption::new);
+        }
+
+        public BaseOptionCollection build() {
+            return new BaseOptionCollection(this);
+        }
+    }
+}
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java
index 171cacac..0298eb52 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/TestingLog.java
@@ -26,6 +26,7 @@ import org.apache.rat.utils.Log;
 public class TestingLog implements Log {
 
     private StringBuilder captured = new StringBuilder();
+    private Log.Level level = Log.Level.INFO;
 
     /**
      * Clears the captured buffer
@@ -86,12 +87,18 @@ public class TestingLog implements Log {
 
     @Override
     public Level getLevel() {
-        return Level.DEBUG;
+        return level;
+    }
+
+    @Override
+    public void setLevel(Level level) {
+        this.level = level;
     }
 
     @Override
     public void log(Level level, String msg) {
-        captured.append(String.format("%s: %s%n", level, msg));
+        if (isEnabled(level))
+            captured.append(String.format("%s: %s%n", level, msg));
     }
 
     /**
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java
index 94b23a35..6bc5682f 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/AbstractTestDataProvider.java
@@ -32,7 +32,7 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.rat.OptionCollectionParser;
 import org.apache.rat.commandline.Arg;
-import org.apache.rat.ui.AbstractOptionCollection;
+import org.apache.rat.ui.UIOptionCollection;
 import org.apache.rat.ui.ArgumentTracker;
 import org.apache.rat.utils.DefaultLog;
 
@@ -40,8 +40,6 @@ import org.apache.rat.utils.DefaultLog;
  * Generates a list of TestData for executing the Report.
  * Use of this interface ensures consistent testing across the UIs. Each method
  * tests an Option from {@link OptionCollectionParser} that must be implemented in the UI.
- * This differes from the {@link OptionTestDataProvider} in that tests from this set
- * expect that execptions will be thrown during execution, and tests the xml output.
  */
 public abstract class AbstractTestDataProvider {
 
@@ -56,7 +54,7 @@ public abstract class AbstractTestDataProvider {
      * @param optionCollection the collection of options for the UI under test.
      * @return the map of testName to Test Data.
      */
-    public final Map<String, TestData> getOptionTestMap(final AbstractOptionCollection<?> optionCollection) {
+    public final Map<String, TestData> getOptionTestMap(final UIOptionCollection<?> optionCollection) {
         Map<String, TestData> map = new TreeMap<>();
         for (TestData test : getOptionTests(optionCollection)) {
             map.put(test.getTestName(), test);
@@ -71,7 +69,7 @@ public abstract class AbstractTestDataProvider {
      * @param optionCollection the collection of options for the UI under test.
      * @return a set of TestData for the tests.
      */
-    public final Set<TestData> getOptionTests(final AbstractOptionCollection<?> optionCollection) {
+    public final Set<TestData> getOptionTests(final UIOptionCollection<?> optionCollection) {
         // the optionCollection establishes any changes to the Arg values.
         List<Option> unsupportedOptions = new ArrayList<>(optionCollection.getUnsupportedOptions().getOptions());
         Set<TestData> result = new TreeSet<>();
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/DataUtils.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/DataUtils.java
new file mode 100644
index 00000000..11ad8ec8
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/DataUtils.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ */
+package org.apache.rat.testhelpers.data;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Locale;
+import java.util.function.Consumer;
+import org.apache.commons.cli.Option;
+import org.apache.rat.report.xml.writer.XmlWriter;
+import org.apache.rat.utils.CasedString;
+import org.apache.rat.utils.DefaultLog;
+
+public class DataUtils {
+    /**
+     * The text for the current ASF license.
+     */
+    public static final String[] ASF_TEXT_LINES = {
+            "Licensed to the Apache Software Foundation (ASF) under one or more",
+            "contributor license agreements.  See the NOTICE file distributed with",
+            "this work for additional information regarding copyright ownership.",
+            "The ASF licenses this file to You under the Apache License, Version 2.0",
+            "(the \"License\"); you may not use this file except in compliance with",
+            "the License.  You may obtain a copy of the License at",
+            "  ",
+            "    https://www.apache.org/licenses/LICENSE-2.0",
+            "  ",
+            "Unless required by applicable law or agreed to in writing, software",
+            "distributed under the License is distributed on an \"AS IS\" BASIS,",
+            "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
+            "See the License for the specific language governing permissions and",
+            "limitations under the License."
+    };
+
+    /**
+     * The text for the current ASF license as a block of text with new lines.
+     */
+    public static final String ASF_TEXT = String.join("\n" ,ASF_TEXT_LINES);
+
+    /**
+     * A setup that does nothing.
+     */
+    public static final Consumer<Path> NO_SETUP = basePath -> {
+        DefaultLog.getInstance().warn("no setup for " + basePath);
+    };
+
+    public static final Consumer<ValidatorData> NO_VALIDATOR = validatorData -> {};
+
+    private DataUtils() {
+        // do not instantiate
+    }
+
+    /**
+     * Create a directory name from the option.  Directory names are camel.
+     * @param option the option to create a directory name from.
+     * @return the directory name for the option.
+     */
+    static String asDirName(Option option) {
+        if (option.hasLongOpt()) {
+            return CasedString.StringCase.CAMEL.assemble(CasedString.StringCase.KEBAB.getSegments(option.getLongOpt()));
+        }
+        return option.getOpt().toLowerCase(Locale.ROOT);
+    }
+
+    /**
+     * Generate a simple configuration file that defines one license based on the name of the file and
+     * containing as single text matcher that mateches the {@code text} parameter.
+     * @param fileName The name of the file to create.
+     * @param id the ID for the family.
+     * @param text the text for the matcher.
+     */
+    static void generateTextConfig(Path fileName, String id, String text) {
+        try (XmlWriter writer = new XmlWriter(new OutputStreamWriter(Files.newOutputStream(fileName.toFile().toPath()),
+                        StandardCharsets.UTF_8))) {
+            writer.startDocument()
+                    .comment(ASF_TEXT)
+                    .openElement("rat-config")
+                    .openElement("families")
+                    .openElement("family")
+                    .attribute("id", id)
+                    .attribute("name", "from " + fileName.getFileName())
+                    .closeElement() // family
+                    .closeElement() // families
+                    .openElement("licenses")
+                    .openElement("license")
+                    .attribute("family", id)
+                    .openElement("text")
+                    .content(text)
+                    .closeDocument(); // close all open elements
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    /**
+     * Generate a simple configuration file that defines one license based on the name of the file and
+     * containing as single text matcher that mateches the {@code text} parameter.
+     * @param fileName The name of the file to create.
+     * @param id the ID for the family.
+     * @param spdxId the SPDX id for the matcher.
+     */
+    static void generateSpdxConfig(Path fileName, String id, String spdxId) {
+        try (XmlWriter writer = new XmlWriter(new OutputStreamWriter(Files.newOutputStream(fileName.toFile().toPath()),
+                StandardCharsets.UTF_8))) {
+            writer.startDocument()
+                    .comment(ASF_TEXT)
+                    .openElement("rat-config")
+                    .openElement("families")
+                    .openElement("family")
+                    .attribute("id", id)
+                    .attribute("name", "from " + fileName.getFileName())
+                    .closeElement() // family
+                    .closeElement() // families
+                    .openElement("licenses")
+                    .openElement("license")
+                    .attribute("family", id)
+                    .openElement("spdx")
+                    .attribute("name", spdxId)
+                    .closeDocument(); // close all open elements
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ReportTestDataProvider.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ReportTestDataProvider.java
new file mode 100644
index 00000000..399ed50d
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ReportTestDataProvider.java
@@ -0,0 +1,1282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   https://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ */
+package org.apache.rat.testhelpers.data;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.apache.commons.cli.Option;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.rat.OptionCollectionParser;
+import org.apache.rat.ReportConfiguration;
+import org.apache.rat.api.RatException;
+import org.apache.rat.commandline.Arg;
+import org.apache.rat.commandline.StyleSheets;
+import org.apache.rat.config.exclusion.StandardCollection;
+import org.apache.rat.config.results.ClaimValidator;
+import org.apache.rat.document.DocumentName;
+import org.apache.rat.license.LicenseSetFactory;
+import org.apache.rat.report.claim.ClaimStatistic;
+import org.apache.rat.report.xml.writer.XmlWriter;
+import org.apache.rat.utils.FileUtils;
+import org.apache.rat.testhelpers.TestingLog;
+import org.apache.rat.testhelpers.TextUtils;
+import org.apache.rat.testhelpers.XmlUtils;
+import org.apache.rat.utils.DefaultLog;
+import org.apache.rat.utils.Log;
+import org.w3c.dom.Document;
+
+import static org.apache.rat.utils.FileUtils.writeFile;
+import static org.apache.rat.testhelpers.data.DataUtils.NO_SETUP;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Fail.fail;
+
+/**
+ * Generates a list of TestData for executing the Report.
+ * Use of this interface ensures consistent testing across the UIs. Each method
+ * tests an Option from {@link OptionCollectionParser} that must be implemented in the UI.
+ * This differes from the {@link OptionTestDataProvider} in that tests from this set
+ * expect that execptions will be thrown during execution, and tests the xml output.
+ */
+public class ReportTestDataProvider extends AbstractTestDataProvider {
+
+    public static final RatException NO_LICENSES_EXCEPTION = new RatException("At least one license must be defined");
+
+    private final XPath xpath = XPathFactory.newInstance().newXPath();
+
+    private final Consumer<Path> mkRat = basePath -> {
+        DefaultLog.getInstance().warn("mkRat setup for " + basePath);
+        File baseDir = basePath.toFile();
+        File ratDir = new File(baseDir, ".rat");
+        FileUtils.mkDir(ratDir);
+    };
+
+    private void assertStandardFile(Document document, String fname) {
+        try {
+            XmlUtils.assertIsPresent(document, xpath,
+                    String.format("/rat-report/resource[@name='/%s'][@type='STANDARD']", fname));
+        } catch (XPathExpressionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void assertIgnoredFile(Document document, String fname) {
+        try {
+            XmlUtils.assertIsPresent(document, xpath,
+                    String.format("/rat-report/resource[@name='/%s'][@type='IGNORED']", fname));
+        } catch (XPathExpressionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    private void assertCounter(ValidatorData data, ClaimStatistic.Counter counter, int count) {
+        assertThat(data.getStatistic().getCounter(counter)).as(counter.name()).isEqualTo(count);
+    }
+
+    // exclude tests
+    private List<TestData> execExcludeTest(final Option option, final Supplier<String[]> args, Consumer<Path> setupFiles) {
+        Consumer<Path> setup = setupFiles.andThen(mkRat).andThen(basePath -> {
+            DefaultLog.getInstance().warn("execExcludeTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "notbaz");
+            writeFile(baseDir, "well._afile");
+            writeFile(baseDir, "some.foo");
+            writeFile(baseDir, "B.bar");
+            writeFile(baseDir, "justbaz");
+        });
+
+        TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execExcludeTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 5);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 1);
+                });
+
+        String[] ignored = {"B.bar", "justbaz", "some.foo", ".rat"};
+        String[] standard = {"well._afile", "notbaz"};
+        TestData test2 = new TestData("", Collections.singletonList(ImmutablePair.of(option, args.get())),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execExcludeTest for " + validatorData.getBaseDir());
+                    ClaimStatistic claimStatistic = validatorData.getStatistic();
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, standard.length);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, ignored.length);
+                    for (String fileName : ignored) {
+                        assertIgnoredFile(validatorData.getDocument(), fileName);
+                    }
+                    for (String fileName : standard) {
+                        assertStandardFile(validatorData.getDocument(), fileName);
+                    }
+                });
+        return Arrays.asList(test1, test2);
+    }
+
+    protected void inputExcludeFileTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = mkRat.andThen(baseDir -> {
+            File dir = baseDir.resolve(".rat").toFile();
+            writeFile(dir, "exclude.txt", Arrays.asList(AbstractTestDataProvider.EXCLUDE_ARGS));
+        });
+        Supplier<String[]> args = () -> new String[]{".rat/exclude.txt"};
+        result.addAll(execExcludeTest(option, args, setup));
+    }
+
+
+    protected void inputExcludeTest(final Set<TestData> result, final Option option) {
+        result.addAll(execExcludeTest(option, () -> AbstractTestDataProvider.EXCLUDE_ARGS, x -> {
+        }));
+    }
+
+    protected void inputExcludeStdTest(final Set<TestData> result, final Option option) {
+        String[] args = {StandardCollection.MAVEN.name()};
+        String[] defaultExcluded = {"afile~", ".#afile", "%afile%", "._afile"};
+        String[] defaultIncluded = {"afile~more", "what.#afile", "%afile%withMore", "well._afile"};
+        String mavenFile = "build.log";
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("inputExcludeStdTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            for (String fileName : defaultExcluded) {
+                writeFile(baseDir, fileName);
+            }
+            for (String fileName : defaultIncluded) {
+                writeFile(baseDir, fileName);
+            }
+            writeFile(baseDir, mavenFile);
+        };
+        result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.nullPair()),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputExcludeStdTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, defaultIncluded.length + 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, defaultExcluded.length);
+                    for (String fileName : defaultIncluded) {
+                        assertStandardFile(validatorData.getDocument(), fileName);
+                    }
+                    for (String fileName : defaultExcluded) {
+                        assertIgnoredFile(validatorData.getDocument(), fileName);
+                    }
+                }));
+
+
+        result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, args)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputExcludeStdTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, defaultIncluded.length);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, defaultExcluded.length + 1);
+                    for (String fileName : defaultIncluded) {
+                        assertStandardFile(validatorData.getDocument(), fileName);
+                    }
+                    for (String fileName : defaultExcluded) {
+                        assertIgnoredFile(validatorData.getDocument(), fileName);
+                    }
+                    assertIgnoredFile(validatorData.getDocument(), mavenFile);
+                }));
+    }
+
+    protected void inputExcludeParsedScmTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("inputExcludeParsedScmTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            String[] lines = {
+                    "# somethings",
+                    "!thingone", "thing*", System.lineSeparator(),
+                    "# some fish",
+                    "**/fish", "*_fish",
+                    "# some colorful directories",
+                    "red/", "blue/*/"};
+            writeFile(baseDir, ".gitignore", Arrays.asList(lines));
+            writeFile(baseDir, "thingone");
+            writeFile(baseDir, "thingtwo");
+            File dir = new File(baseDir, "dir");
+            FileUtils.mkDir(dir);
+            FileUtils.writeFile(dir, "fish_two");
+            FileUtils.writeFile(dir, "fish");
+
+            dir = new File(baseDir, "red");
+            FileUtils.mkDir(dir);
+            FileUtils.writeFile(dir, "fish");
+
+            dir = new File(baseDir, "blue/fish");
+            FileUtils.mkDir(dir);
+            FileUtils.writeFile(dir, "dory");
+
+            dir = new File(baseDir, "some");
+            FileUtils.mkDir(dir);
+            FileUtils.writeFile(dir, "fish");
+            FileUtils.writeFile(dir, "things");
+            FileUtils.writeFile(dir, "thingone");
+
+            dir = new File(baseDir, "another");
+            FileUtils.mkDir(dir);
+            FileUtils.writeFile(dir, "red_fish");
+        };
+
+        result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.nullPair()),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputExcludeParsedScmTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 11);
+                    // .gitignore is ignored by default as it is hidden
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0);
+                }));
+
+        result.add(new TestData("GIT", Collections.singletonList(ImmutablePair.of(option, new String[]{"GIT"})),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputExcludeParsedScmTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3);
+                    // .gitignore is ignored by default as it is hidden
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 8);
+                }));
+    }
+
+    protected void inputExcludeSizeTest(final Set<TestData> result, final Option option) {
+        String[] notExcluded = {"Hello.txt", "HelloWorld.txt"};
+        String[] excluded = {"Hi.txt"};
+
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("inputExcludeSizeTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "Hi.txt", Collections.singletonList("Hi"));
+            writeFile(baseDir, "Hello.txt", Collections.singletonList("Hello"));
+            writeFile(baseDir, "HelloWorld.txt", Collections.singletonList("HelloWorld"));
+        };
+
+        result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.of(null, null)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputExcludeSizeTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0);
+                    for (String fname : excluded) {
+                        assertStandardFile(validatorData.getDocument(), fname);
+                    }
+                    for (String fname : notExcluded) {
+                        assertStandardFile(validatorData.getDocument(), fname);
+                    }
+                }));
+
+        result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"5"})),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputExcludeSizeTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 1);
+                    for (String fname : excluded) {
+                        assertIgnoredFile(validatorData.getDocument(), fname);
+                    }
+                    for (String fname : notExcluded) {
+                        assertStandardFile(validatorData.getDocument(), fname);
+                    }
+                }));
+    }
+
+    // include tests
+    private List<TestData> execIncludeTest(final Option option, String[] args, Consumer<Path> setupFiles) {
+        Option excludeOption = Arg.EXCLUDE.option();
+        Consumer<Path> setup = setupFiles.andThen(basePath -> {
+            DefaultLog.getInstance().warn("execIncludeTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "notbaz");
+            writeFile(baseDir, "some.foo");
+            writeFile(baseDir, "B.bar");
+            writeFile(baseDir, "justbaz");
+        });
+
+        // standard without options.
+        TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execIncludeTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 4);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 1);
+                });
+
+        // verify exclude removes the files
+        TestData test2 = new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.of(excludeOption, AbstractTestDataProvider.EXCLUDE_ARGS)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execIncludeTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    // .gitignore is ignored by default as it is hidden but not counted
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 4);
+                });
+
+        TestData test3 = new TestData("", Arrays.asList(ImmutablePair.of(option, args), ImmutablePair.of(excludeOption, AbstractTestDataProvider.EXCLUDE_ARGS)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execIncludeTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3);
+                    // .gitignore is ignored by default as it is hidden but not counted
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 2);
+                });
+        return Arrays.asList(test1, test2, test3);
+    }
+
+    protected void inputIncludeFileTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = mkRat.andThen(basePath -> {
+            DefaultLog.getInstance().warn("inputIncludeFileTest setup for " + basePath);
+            File dir = basePath.resolve(".rat").toFile();
+            writeFile(dir, "include.txt", Arrays.asList(AbstractTestDataProvider.INCLUDE_ARGS));
+        });
+        result.addAll(execIncludeTest(option, new String[]{".rat/include.txt"}, setup));
+    }
+
+
+    protected void inputIncludeTest(final Set<TestData> result, final Option option) {
+        result.addAll(execIncludeTest(option, AbstractTestDataProvider.INCLUDE_ARGS, mkRat));
+    }
+
+    protected void inputIncludeStdTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("inputIncludeStdTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "afile~more");
+            writeFile(baseDir, "afile~");
+            writeFile(baseDir, ".#afile");
+            writeFile(baseDir, "%afile%");
+            writeFile(baseDir, "._afile");
+            writeFile(baseDir, "what.#afile");
+            writeFile(baseDir, "%afile%withMore");
+            writeFile(baseDir, "well._afile");
+            writeFile(baseDir, ".hiddenFile", "The hidden file");
+            File hiddenDir = new File(baseDir, ".hiddenDir");
+            FileUtils.mkDir(hiddenDir);
+            writeFile(hiddenDir, "aFile", "File in hidden directory");
+        };
+        if (Arg.EXCLUDE.isEmpty()) {
+            throw new RuntimeException(String.format("Can not test %s if there are no exclude file options supported", option.getKey()));
+        }
+        ImmutablePair<Option, String[]> excludes = ImmutablePair.of(Arg.EXCLUDE.option(),
+                new String[]{"*~more", "*~"});
+
+
+        result.add(new TestData("includeStdValidation", Collections.singletonList(excludes),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 4);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 6);
+                }));
+
+        if (!option.hasArg()) {
+            if (option.getKey().equals("scan-hidden-directories")) {
+                result.add(new TestData("",
+                        Arrays.asList(ImmutablePair.of(option, null), excludes),
+                        setup,
+                        validatorData -> {
+                            DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir());
+                            assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 5);
+                            assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 5);
+                            assertIgnoredFile(validatorData.getDocument(), "._afile");
+                            assertStandardFile(validatorData.getDocument(), ".hiddenDir/aFile");
+                        }));
+            } else {
+                throw new RuntimeException("Unknown option: " + option.getKey());
+            }
+        } else {
+            result.add(new TestData(StandardCollection.MISC.name().toLowerCase(Locale.ROOT), Arrays.asList(ImmutablePair.of(option, new String[]{StandardCollection.MISC.name()}), excludes),
+                    setup,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir());
+                        assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 8);
+                        assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 2);
+                    }));
+
+            result.add(new TestData(StandardCollection.HIDDEN_FILE.name().toLowerCase(Locale.ROOT),
+                    Arrays.asList(ImmutablePair.of(option, new String[]{StandardCollection.HIDDEN_FILE.name()}), excludes),
+                    setup,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir());
+                        assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 6);
+                        assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 4);
+                        assertStandardFile(validatorData.getDocument(), "._afile");
+                        assertIgnoredFile(validatorData.getDocument(), ".hiddenDir");
+                    }));
+
+            result.add(new TestData(StandardCollection.HIDDEN_DIR.name().toLowerCase(Locale.ROOT),
+                    Arrays.asList(ImmutablePair.of(option, new String[]{StandardCollection.HIDDEN_DIR.name()}), excludes),
+                    setup,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating inputIncludeStdTest for " + validatorData.getBaseDir());
+                        assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 5);
+                        assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 5);
+                        assertIgnoredFile(validatorData.getDocument(), "._afile");
+                        assertStandardFile(validatorData.getDocument(), ".hiddenDir/aFile");
+                    }));
+        }
+    }
+
+    protected void inputSourceTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("inputSourceTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "codefile", "https://www.apache.org/licenses/LICENSE-2.0");
+            writeFile(baseDir, "input.txt", "codefile");
+            writeFile(baseDir, "notcodeFile");
+        };
+
+        result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputSourceTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 3);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0);
+                }));
+
+        result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"input.txt"})),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating inputSourceTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.IGNORED, 0);
+                }));
+    }
+
+    // LICENSE tests
+    protected List<TestData> execLicensesApprovedTest(final Option option, String[] args, Consumer<Path> extraSetup) {
+        Consumer<Path> setup = extraSetup.andThen(
+                basePath -> {
+                    DefaultLog.getInstance().warn("execLicensesApprovedTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "catz.txt", "SPDX-License-Identifier: catz");
+                    writeFile(baseDir, "apl.txt", "SPDX-License-Identifier: Apache-2.0");
+                }
+        );
+
+        TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicensesApprovedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                });
+
+        TestData test2 = new TestData("withoutLicenseDef", Collections.singletonList(ImmutablePair.of(option, args)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicensesApprovedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                });
+
+        Option configOpt = Arg.CONFIGURATION.option();
+        TestData test3 = new TestData("withLicenseDef", Arrays.asList(ImmutablePair.of(option, args),
+                ImmutablePair.of(configOpt, new String[]{".rat/catz.xml"})),
+                setup.andThen(mkRat).andThen(
+                        basePath -> {
+                            DefaultLog.getInstance().warn("withLicenseDef setup for " + basePath);
+                            DataUtils.generateSpdxConfig(basePath.resolve(".rat").resolve("catz.xml"), "catz", "catz");
+                        }),
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicensesApprovedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0);
+                });
+        return Arrays.asList(test1, test2, test3);
+    }
+
+    protected void helpLicenses(final Set<TestData> result, final Option option) {
+        PrintStream origin = System.out;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        PrintStream out = new PrintStream(baos);
+
+        result.add(new TestData("stdOut",
+                Collections.singletonList(ImmutablePair.of(option, null)),
+                basePath -> {
+                    DefaultLog.getInstance().warn("helpLicenses setup for " + basePath);
+                    System.setOut(out);
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating helpLicenses for " + validatorData.getBaseDir());
+                    System.setOut(origin);
+                    String txt = baos.toString();
+                    TextUtils.assertContains("====== Licenses ======", txt);
+                    TextUtils.assertContains("====== Defined Matchers ======", txt);
+                    TextUtils.assertContains("====== Defined Families ======", txt);
+                }));
+    }
+
+    protected void licensesApprovedFileTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicensesApprovedTest(option, new String[]{".rat/licensesApproved.txt"},
+                mkRat.andThen(
+                        basePath -> {
+                            DefaultLog.getInstance().warn("licensesApprovedFileTest setup for " + basePath);
+                            writeFile(basePath.resolve(".rat").toFile(), "licensesApproved.txt", List.of("catz"));
+                        })));
+    }
+
+    protected void licensesApprovedTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicensesApprovedTest(option, new String[]{"catz"}, NO_SETUP));
+    }
+
+    private List<TestData> execLicensesDeniedTest(final Option option, final String[] args, Consumer<Path> setupFiles) {
+        Consumer<Path> setup = setupFiles.andThen(basePath -> {
+            DefaultLog.getInstance().warn("execLicensesDeniedTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "illumousFile.java", "The contents of this file are " +
+                    "subject to the terms of the Common Development and Distribution License (the \"License\") You " +
+                    "may not use this file except in compliance with the License.");
+        });
+        TestData test1 = new TestData("ILLUMOS", Collections.singletonList(ImmutablePair.of(option, args)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicensesDeniedTest for " + validatorData.getBaseDir());
+                    ClaimStatistic claimStatistic = validatorData.getStatistic();
+                    ClaimValidator validator = validatorData.getConfiguration().getClaimValidator();
+                    assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
+                });
+        return Collections.singletonList(test1);
+    }
+
+    protected void licensesDeniedTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicensesDeniedTest(option, new String[]{"ILLUMOS"}, NO_SETUP));
+    }
+
+    protected void licensesDeniedFileTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicensesDeniedTest(option, new String[]{"licensesDenied.txt"},
+                (basePath) -> writeFile(basePath.toFile(), "licensesDenied.txt", Collections.singletonList("ILLUMOS"))));
+    }
+
+    private List<TestData> execLicenseFamiliesApprovedTest(final Option option, final String[] args, Consumer<Path> extraSetup) {
+        Consumer<Path> setup = extraSetup.andThen(
+                basePath -> {
+                    DefaultLog.getInstance().warn("execLicenseFamiliesApprovedTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "catz.txt", "SPDX-License-Identifier: catz");
+                });
+
+        TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicenseFamiliesApprovedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 0);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                });
+
+        TestData test2 = new TestData("withoutLicenseDef", Collections.singletonList(ImmutablePair.of(option, args)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicenseFamiliesApprovedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 0);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                });
+        Option configOpt = Arg.CONFIGURATION.option();
+        TestData test3 = new TestData("withLicenseDef", Arrays.asList(ImmutablePair.of(option, args),
+                ImmutablePair.of(configOpt, new String[]{".rat/catz.xml"})),
+                setup.andThen(mkRat).andThen(
+                        basePath -> {
+                            DefaultLog.getInstance().warn("withLicenseDef setup for " + basePath);
+                            Path ratDir = basePath.resolve(".rat");
+                            Path catzXml = ratDir.resolve("catz.xml");
+                            DataUtils.generateSpdxConfig(catzXml, "catz", "catz");
+                        }),
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicenseFamiliesApprovedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0);
+                });
+        return Arrays.asList(test1, test2, test3);
+    }
+
+    protected void licenseFamiliesApprovedFileTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicenseFamiliesApprovedTest(option, new String[]{".rat/familiesApproved.txt"},
+                mkRat.andThen(basePath -> {
+                    DefaultLog.getInstance().warn("licenseFamiliesApprovedFileTest setup for " + basePath);
+                    writeFile(basePath.resolve(".rat").toFile(), "familiesApproved.txt", Collections.singletonList("catz"));
+                })));
+    }
+
+    protected void licenseFamiliesApprovedTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicenseFamiliesApprovedTest(option, new String[]{"catz"}, NO_SETUP));
+    }
+
+    private List<TestData> execLicenseFamiliesDeniedTest(final Option option, final String[] args, Consumer<Path> extraSetup) {
+        Consumer<Path> setup = extraSetup.andThen(
+                basePath -> {
+                    DefaultLog.getInstance().warn("execLicenseFamiliesDeniedTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "bsd.txt", "SPDX-License-Identifier: BSD-3-Clause");
+                });
+
+        TestData test1 = new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicenseFamiliesDeniedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0);
+                });
+
+        TestData test2 = new TestData("", Collections.singletonList(ImmutablePair.of(option, args)),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating execLicenseFamiliesDeniedTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 0);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                });
+
+        return Arrays.asList(test1, test2);
+    }
+
+    protected void licenseFamiliesDeniedFileTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicenseFamiliesDeniedTest(option, new String[]{".rat/familiesDenied.txt"},
+                mkRat.andThen(
+                        basePath -> {
+                            DefaultLog.getInstance().warn("licenseFamiliesDeniedFileTest setup for " + basePath);
+                            writeFile(basePath.resolve(".rat").toFile(), "familiesDenied.txt", Collections.singletonList("BSD-3"));
+                        })));
+    }
+
+    protected void licenseFamiliesDeniedTest(final Set<TestData> result, final Option option) {
+        result.addAll(execLicenseFamiliesDeniedTest(option, new String[]{"BSD-3"}, NO_SETUP));
+    }
+
+    protected void counterMaxTest(final Set<TestData> result, final Option option) {
+        result.add(new TestData(DataUtils.asDirName(option), Collections.singletonList(ImmutablePair.of(null, null)),
+                basePath -> {
+                    DefaultLog.getInstance().warn("counterMaxTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n",
+                            "*/\n\n", "class Test {}\n"));
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating counterMaxTest for " + validatorData.getBaseDir());
+                    ClaimStatistic claimStatistic = validatorData.getStatistic();
+                    ClaimValidator validator = validatorData.getConfiguration().getClaimValidator();
+                    assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
+                }));
+
+        result.add(new TestData("Unapproved1", Collections.singletonList(ImmutablePair.of(option, new String[]{"Unapproved:1"})),
+                basePath -> {
+                    DefaultLog.getInstance().warn("counterMaxTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n",
+                            "*/\n\n", "class Test {}\n"));
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating counterMaxTest for " + validatorData.getBaseDir());
+                    ClaimStatistic claimStatistic = validatorData.getStatistic();
+                    ClaimValidator validator = validatorData.getConfiguration().getClaimValidator();
+                    assertThat(validator.listIssues(claimStatistic)).isEmpty();
+                }));
+    }
+
+    protected void counterMinTest(final Set<TestData> result, final Option option) {
+        result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                basePath -> {
+                    DefaultLog.getInstance().warn("counterMinTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n",
+                            "*/\n\n", "class Test {}\n"));
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating counterMinTest for " + validatorData.getBaseDir());
+                    ClaimStatistic claimStatistic = validatorData.getStatistic();
+                    ClaimValidator validator = validatorData.getConfiguration().getClaimValidator();
+                    assertThat(validator.listIssues(claimStatistic)).containsExactly("UNAPPROVED");
+                }));
+
+        result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"Unapproved:1"})),
+                basePath -> {
+                    DefaultLog.getInstance().warn("counterMinTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Unapproved\n",
+                            "*/\n\n", "class Test {}\n"));
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating counterMinTest for " + validatorData.getBaseDir());
+                    ClaimStatistic claimStatistic = validatorData.getStatistic();
+                    ClaimValidator validator = validatorData.getConfiguration().getClaimValidator();
+                    assertThat(validator.listIssues(claimStatistic)).isEmpty();
+                }));
+    }
+
+    /**
+     * Add results to the result list.
+     *
+     * @param result the result list.
+     * @param option configuration option we are testing.
+     */
+    protected void configTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = mkRat.andThen(basePath -> {
+            DefaultLog.getInstance().warn("configTest setup for " + basePath);
+            Path ratDir = basePath.resolve(".rat");
+            Path oneXml = ratDir.resolve("One.xml");
+            DataUtils.generateTextConfig(oneXml, "ONE", "one");
+
+            Path twoXml = ratDir.resolve("Two.xml");
+            DataUtils.generateTextConfig(twoXml, "TWO", "two");
+
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "bsd.txt", "SPDX-License-Identifier: BSD-3-Clause");
+            writeFile(baseDir, "one.txt", "one is the loneliest number");
+        });
+        String[] args = {".rat/One.xml", ".rat/Two.xml"};
+
+        ImmutablePair<Option, String[]> underTest = ImmutablePair.of(option, args);
+
+        result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating configTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                }));
+
+        result.add(new TestData("withDefaults", Collections.singletonList(underTest),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating configTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 2);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0);
+                }));
+
+        if (!Arg.CONFIGURATION_NO_DEFAULTS.isEmpty()) {
+            result.add(new TestData("noDefaults", Arrays.asList(underTest,
+                    ImmutablePair.of(Arg.CONFIGURATION_NO_DEFAULTS.find("configuration-no-defaults"), null)),
+                    setup,
+                    /** Make validator data a structure with counter countes  and file checks. */
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating configTest for " + validatorData.getBaseDir());
+                        assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 2);
+                        assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                        assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 1);
+                    }));
+        }
+    }
+
+    protected void configurationNoDefaultsTest(final Set<TestData> result, final Option option) {
+        TestData test1 = new TestData("", Collections.singletonList(ImmutablePair.of(option, null)),
+                basePath -> {
+                    DefaultLog.getInstance().warn("configurationNoDefaultsTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n",
+                            "*/\n\n", "class Test {}\n"));
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating configurationNoDefaultsTest for " + validatorData.getBaseDir());
+                    assertThat(validatorData.getConfiguration().getLicenses(LicenseSetFactory.LicenseFilter.ALL)).isEmpty();
+                });
+        test1.setException(NO_LICENSES_EXCEPTION);
+        result.add(test1);
+    }
+
+    protected void dryRunTest(final Set<TestData> result, final Option option) {
+        result.add(new TestData("stdRun", Collections.singletonList(ImmutablePair.of(option, null)),
+                NO_SETUP,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating dryRunTest for " + validatorData.getBaseDir());
+                    assertThat(validatorData.getConfiguration().isDryRun()).as("testing getConfiguration().isDryRun() flag").isTrue();
+                }));
+
+        result.add(new TestData(DataUtils.asDirName(option), NO_OPTIONS,
+                NO_SETUP,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating dryRunTest for " + validatorData.getBaseDir());
+                    assertThat(validatorData.getConfiguration().isDryRun()).as("testing getConfiguration().isDryRun() flag").isFalse();
+                }));
+    }
+
+    protected void editCopyrightTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("editCopyrightTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n"));
+        };
+        ImmutablePair<Option, String[]> copyright = ImmutablePair.of(option, new String[]{"MyCopyright"});
+        if (Arg.EDIT_ADD.isEmpty()) {
+            throw new RuntimeException("Can not execute copyright tests without an EDIT_ADD option avialable");
+        }
+        ImmutablePair<Option, String[]> editLicense = ImmutablePair.of(Arg.EDIT_ADD.option(), null);
+
+        result.add(new TestData("noEditLicense", Collections.singletonList(copyright),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir());
+                    try {
+                        String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile());
+                        TextUtils.assertNotContains("MyCopyright", actualText);
+                    } catch (IOException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                }));
+
+        result.add(new TestData(DataUtils.asDirName(editLicense.getLeft()), Arrays.asList(copyright, editLicense),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir());
+                    try {
+                        String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile());
+                        TextUtils.assertNotContains("MyCopyright", actualText);
+                        assertThat(validatorData.getBaseDir().resolve("Missing.java.new")).exists();
+                        actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java.new").toFile());
+                        TextUtils.assertContains("MyCopyright", actualText);
+                    } catch (IOException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                }));
+
+        if (!Arg.DRY_RUN.isEmpty()) {
+            Option dryRun = Arg.DRY_RUN.option();
+            result.add(new TestData(DataUtils.asDirName(dryRun),
+                    Arrays.asList(copyright, ImmutablePair.of(dryRun, null), editLicense),
+                    setup,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir());
+                        try {
+                            String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile());
+                            TextUtils.assertNotContains("MyCopyright", actualText);
+                        } catch (IOException ex) {
+                            throw new RuntimeException(ex);
+                        }
+                        assertThat(validatorData.getBaseDir().resolve("Missing.java.new")).doesNotExist();
+                    }));
+        }
+
+        if (!Arg.EDIT_OVERWRITE.isEmpty()) {
+            Option overwrite = Arg.EDIT_OVERWRITE.option();
+            result.add(new TestData(DataUtils.asDirName(overwrite), Arrays.asList(copyright, editLicense, ImmutablePair.of(overwrite, null)),
+                    setup,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating editCopyrightTest for " + validatorData.getBaseDir());
+                        try {
+                            String actualText = TextUtils.readFile(validatorData.getBaseDir().resolve("Missing.java").toFile());
+                            TextUtils.assertContains("MyCopyright", actualText);
+                            assertThat(validatorData.getBaseDir().resolve("Missing.java.new")).doesNotExist();
+                        } catch (IOException ex) {
+                            throw new RuntimeException(ex);
+                        }
+                    }));
+        }
+    }
+
+    protected void editLicenseTest(final Set<TestData> result, final Option option) {
+        result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, null)),
+                basePath -> {
+                    DefaultLog.getInstance().warn("editLicenseTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "NoLicense.java", "class NoLicense {}");
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating editLicenseTest for " + validatorData.getBaseDir());
+                    try {
+                        assertThat(validatorData.getStatistic()).isNotNull();
+                        File javaFile = validatorData.getBaseDir().resolve("NoLicense.java").toFile();
+                        String contents = String.join("\n", IOUtils.readLines(new FileReader(javaFile)));
+                        assertThat(contents).isEqualTo("class NoLicense {}");
+                        File resultFile = validatorData.getBaseDir().resolve("NoLicense.java.new").toFile();
+                        assertThat(resultFile).exists();
+                        contents = String.join("\n", IOUtils.readLines(new FileReader(resultFile)));
+                        assertThat(contents).isEqualTo("""
+                                /*
+                                 * Licensed to the Apache Software Foundation (ASF) under one
+                                 * or more contributor license agreements.  See the NOTICE file
+                                 * distributed with this work for additional information
+                                 * regarding copyright ownership.  The ASF licenses this file
+                                 * to you under the Apache License, Version 2.0 (the
+                                 * "License"); you may not use this file except in compliance
+                                 * with the License.  You may obtain a copy of the License at
+                                 *\s
+                                 *   http://www.apache.org/licenses/LICENSE-2.0
+                                 *\s
+                                 * Unless required by applicable law or agreed to in writing,
+                                 * software distributed under the License is distributed on an
+                                 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+                                 * KIND, either express or implied.  See the License for the
+                                 * specific language governing permissions and limitations
+                                 * under the License.
+                                 */
+                                
+                                class NoLicense {}""");
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                }));
+    }
+
+    protected void editOverwriteTest(final Set<TestData> result, final Option option) {
+        result.add(new TestData("noEditLicense", Collections.singletonList(ImmutablePair.of(option, null)),
+                NO_SETUP,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating editOverwriteTest for " + validatorData.getBaseDir());
+                    assertThat(validatorData.getConfiguration().isAddingLicensesForced())
+                            .describedAs("Without edit-license should be false").isFalse();
+                }));
+
+        if (!Arg.EDIT_ADD.isEmpty()) {
+            result.add(new TestData("", Arrays.asList(ImmutablePair.of(option, null),
+                    ImmutablePair.of(Arg.EDIT_ADD.find("edit-license"), null)),
+                    NO_SETUP,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating editOverwriteTest for " + validatorData.getBaseDir());
+                        assertThat(validatorData.getConfiguration().isAddingLicensesForced())
+                                .as("testing getConfiguration().isAddingLicensesForced() flag").isTrue();
+                    }));
+        }
+    }
+
+    protected void logLevelTest(final Set<TestData> result, final Option option) {
+        final TestingLog testingLog = new TestingLog();
+
+        Consumer<Path> setup = basePath -> {
+            DefaultLog.getInstance().warn("logLevelTest setup for " + basePath);
+            DefaultLog.setInstance(testingLog);
+            testingLog.clear();
+        };
+
+        result.add(new TestData(Log.Level.INFO.name(),
+                Collections.singletonList(ImmutablePair.of(option, new String[]{Log.Level.INFO.name()})),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating logLevelTest for " + validatorData.getBaseDir());
+                    try {
+                        testingLog.assertNotContains("DEBUG");
+                    } finally {
+                        DefaultLog.setInstance(null);
+                    }
+                }));
+
+        result.add(new TestData(Log.Level.DEBUG.name(),
+                Collections.singletonList(ImmutablePair.of(option, new String[]{Log.Level.DEBUG.name()})),
+                setup,
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating logLevelTest for " + validatorData.getBaseDir());
+                    try {
+                        testingLog.assertContains("DEBUG");
+                    } finally {
+                        DefaultLog.setInstance(null);
+                    }
+                }));
+    }
+
+    protected void outputArchiveTest(final Set<TestData> result, final Option option) {
+        for (ReportConfiguration.Processing processing : ReportConfiguration.Processing.values()) {
+            result.add(new TestData(processing.name().toLowerCase(Locale.ROOT),
+                    Collections.singletonList(ImmutablePair.of(option, new String[]{processing.name()})),
+                    basePath -> {
+                        DefaultLog.getInstance().warn("outputArchiveTest setup for " + basePath);
+                        File localArchive = new File(basePath.toFile(), "dummy.jar");
+                        try (InputStream in = ReportTestDataProvider.class.getResourceAsStream("/tikaFiles/archive/dummy.jar");
+                             OutputStream out = Files.newOutputStream(localArchive.toPath())) {
+                            Objects.requireNonNull(in);
+                            IOUtils.copy(in, out);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                    },
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating outputArchiveTest for " + validatorData.getBaseDir());
+                        Document document = validatorData.getDocument();
+                        try {
+                            XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']");
+                            switch (processing) {
+                                case ABSENCE:
+                                    XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='AL   ']");
+                                    XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='?????']");
+                                    break;
+                                case PRESENCE:
+                                    XmlUtils.assertIsPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='AL   ']");
+                                    XmlUtils.assertIsNotPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='?????']");
+                                    break;
+                                case NOTIFICATION:
+                                    XmlUtils.assertIsNotPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='AL   ']");
+                                    XmlUtils.assertIsNotPresent(processing.name(), document, xpath, "/rat-report/resource[@name='/dummy.jar']/license[@family='?????']");
+                                    break;
+                                default:
+                                    throw new IllegalArgumentException("Unexpected processing " + processing);
+                            }
+                        } catch (XPathExpressionException e) {
+                            throw new RuntimeException(e);
+                        }
+                    })
+            );
+        }
+    }
+
+    protected void outputFamiliesTest(final Set<TestData> result, final Option option) {
+        for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
+            result.add(new TestData(filter.name().toLowerCase(Locale.ROOT),
+                    Collections.singletonList(ImmutablePair.of(option, new String[]{filter.name()})),
+                    NO_SETUP,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating outputFamiliesTest for " + validatorData.getBaseDir());
+                        Document document = validatorData.getDocument();
+                        try {
+                            switch (filter) {
+                                case ALL:
+                                    XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='AL']");
+                                    XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='GPL']");
+                                    break;
+                                case APPROVED:
+                                    XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='AL']");
+                                    XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='GPL']");
+                                    break;
+                                case NONE:
+                                    XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='AL']");
+                                    XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/families/family[@id='GPL']");
+                                    break;
+                                default:
+                                    throw new IllegalArgumentException("Unexpected filter: " + filter);
+                            }
+                        } catch (XPathExpressionException e) {
+                            throw new RuntimeException(e);
+                        }
+                    })
+            );
+        }
+    }
+
+    protected void outputFileTest(final Set<TestData> result, final Option option) {
+        result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, new String[]{"outexample"})),
+                basePath -> {
+                    DefaultLog.getInstance().warn("outputFileTest setup for " + basePath);
+                    File baseDir = basePath.toFile();
+                    writeFile(baseDir, "apl.txt", "SPDX-License-Identifier: Apache-2.0");
+                },
+                validatorData -> {
+                    DefaultLog.getInstance().warn("validating outputFamiliesTest for " + validatorData.getBaseDir());
+                    assertCounter(validatorData, ClaimStatistic.Counter.STANDARDS, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.APPROVED, 1);
+                    assertCounter(validatorData, ClaimStatistic.Counter.UNAPPROVED, 0);
+                    File outFile = validatorData.getBaseDir().resolve("outexample").toFile();
+
+                    try {
+                        String actualText = TextUtils.readFile(outFile);
+                        TextUtils.assertPatternInTarget("Apache License 2.0: \\d", actualText);
+                        TextUtils.assertPatternInTarget("STANDARD:\\s+\\d", actualText);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                })
+        );
+    }
+
+    protected void outputLicensesTest(final Set<TestData> result, final Option option) {
+        for (LicenseSetFactory.LicenseFilter filter : LicenseSetFactory.LicenseFilter.values()) {
+            result.add(new TestData(filter.name(), Collections.singletonList(ImmutablePair.of(option, new String[]{filter.name()})),
+                    NO_SETUP,
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating outputLicensesTest for " + validatorData.getBaseDir());
+                        Document document = validatorData.getDocument();
+                        try {
+                            switch (filter) {
+                                case ALL:
+                                    XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='AL2.0']");
+                                    XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='GPL1']");
+                                    break;
+                                case APPROVED:
+                                    XmlUtils.assertIsPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='AL2.0']");
+                                    XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='GPL1']");
+                                    break;
+                                case NONE:
+                                    XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='AL2.0']");
+                                    XmlUtils.assertIsNotPresent(filter.name(), document, xpath, "/rat-report/rat-config/licenses/license[@id='GPL1']");
+                                    break;
+                                default:
+                                    throw new IllegalArgumentException("Unexpected filter: " + filter);
+                            }
+                        } catch (XPathExpressionException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }));
+        }
+    }
+
+
+    protected void outputStandardTest(final Set<TestData> result, final Option option) {
+        for (ReportConfiguration.Processing proc : ReportConfiguration.Processing.values()) {
+            result.add(new TestData(proc.name().toLowerCase(Locale.ROOT), Collections.singletonList(ImmutablePair.of(option, new String[]{proc.name()})),
+                    basePath -> {
+                        DefaultLog.getInstance().warn("outputStandardTest setup for " + basePath);
+                        File baseDir = basePath.toFile();
+                        writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n",
+                                "*/\n\n", "class Test {}\n"));
+                        writeFile(baseDir, "Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n"));
+                    },
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating outputStandardTest for " + validatorData.getBaseDir());
+                        Document document = validatorData.getDocument();
+                        String testDoc = "/rat-report/resource[@name='/Test.java']";
+                        String missingDoc = "/rat-report/resource[@name='/Missing.java']";
+                        try {
+                            XmlUtils.assertIsPresent(proc.name(), document, xpath, testDoc);
+                            XmlUtils.assertIsPresent(proc.name(), document, xpath, missingDoc);
+
+                            switch (proc) {
+                                case ABSENCE:
+                                    XmlUtils.assertIsPresent(proc.name(), document, xpath, testDoc + "/license[@family='AL   ']");
+                                    XmlUtils.assertIsPresent(proc.name(), document, xpath, missingDoc + "/license[@family='?????']");
+                                    break;
+                                case PRESENCE:
+                                    XmlUtils.assertIsPresent(proc.name(), document, xpath, testDoc + "/license[@family='AL   ']");
+                                    XmlUtils.assertIsNotPresent(proc.name(), document, xpath, missingDoc + "/license[@family='?????']");
+                                    break;
+                                case NOTIFICATION:
+                                    XmlUtils.assertIsNotPresent(proc.name(), document, xpath, testDoc + "/license[@family='AL   ']");
+                                    XmlUtils.assertIsNotPresent(proc.name(), document, xpath, missingDoc + "/license[@family='?????']");
+                                    break;
+                                default:
+                                    throw new IllegalArgumentException("Unexpected processing " + proc);
+                            }
+                        } catch (XPathExpressionException ex) {
+                            throw new RuntimeException(ex);
+                        }
+                    }));
+        }
+    }
+
+
+    protected void outputStyleTest(final Set<TestData> result, final Option option) {
+        Consumer<Path> createFile = basePath -> {
+            DefaultLog.getInstance().warn("outputStyleTest setup for " + basePath);
+            File baseDir = basePath.toFile();
+            writeFile(baseDir, "Test.java", Arrays.asList("/*\n", "SPDX-License-Identifier: Apache-2.0\n",
+                    "*/\n\n", "class Test {}\n"));
+            writeFile(baseDir, "Missing.java", Arrays.asList("/* no license */\n\n", "class Test {}\n"));
+        };
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        if (!option.hasArg()) {
+            if (option.getLongOpt().equals("xml")) {
+                result.add(new TestData("", Collections.singletonList(ImmutablePair.of(option, null)),
+                        createFile,
+                        validatorData -> {
+                            DefaultLog.getInstance().warn("validating outputStyleTest for " + validatorData.getBaseDir());
+                            try (InputStream expected = StyleSheets.XML.getStyleSheet().ioSupplier().get();
+                                 InputStream actual = validatorData.getConfiguration().getStyleSheet().get()) {
+                                assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", StyleSheets.XML)).isTrue();
+                                baos.reset();
+                                validatorData.getOutput().format(validatorData.getConfiguration().getStyleSheet(), () -> baos);
+                                String actualText = baos.toString();
+                                TextUtils.assertContainsExactly(1, "<resource encoding=\"ISO-8859-1\" mediaType=\"text/x-java-source\" name=\"/Test.java\" type=\"STANDARD\">", actualText);
+                                TextUtils.assertContainsExactly(1, "<resource encoding=\"ISO-8859-2\" mediaType=\"text/x-java-source\" name=\"/Missing.java\" type=\"STANDARD\">", actualText);
+                            } catch (IOException | RatException e) {
+                                throw new RuntimeException(e);
+                            }
+                        }));
+            }
+        } else {
+            for (StyleSheets sheet : StyleSheets.values()) {
+                result.add(new TestData(sheet.name().toLowerCase(Locale.ROOT), Collections.singletonList(ImmutablePair.of(option, new String[]{sheet.arg()})),
+                        createFile,
+                        validatorData -> {
+                            DefaultLog.getInstance().warn("validating outputStyleTest for " + validatorData.getBaseDir());
+                            try (InputStream expected = sheet.getStyleSheet().ioSupplier().get();
+                                 InputStream actual = validatorData.getConfiguration().getStyleSheet().get()) {
+                                assertThat(IOUtils.contentEquals(expected, actual)).as(() -> String.format("'%s' does not match", sheet)).isTrue();
+                                baos.reset();
+                                validatorData.getOutput().format(validatorData.getConfiguration().getStyleSheet(), () -> baos);
+                                String actualText = baos.toString();
+                                switch (sheet) {
+                                    case MISSING_HEADERS:
+                                        TextUtils.assertContainsExactly(1, "Files with missing headers:" + System.lineSeparator() +
+                                                "  /Missing.java", actualText);
+                                        break;
+                                    case PLAIN:
+                                        TextUtils.assertContainsExactly(1, "Unknown license: 1 ", actualText);
+                                        TextUtils.assertContainsExactly(1, "?????: 1 ", actualText);
+                                        break;
+                                    case XML:
+                                        TextUtils.assertContainsExactly(1, "<resource encoding=\"ISO-8859-1\" mediaType=\"text/x-java-source\" name=\"/Test.java\" type=\"STANDARD\">", actualText);
+                                        TextUtils.assertContainsExactly(1, "<resource encoding=\"ISO-8859-2\" mediaType=\"text/x-java-source\" name=\"/Missing.java\" type=\"STANDARD\">", actualText);
+                                        break;
+                                    case UNAPPROVED_LICENSES:
+                                        TextUtils.assertContainsExactly(1, "Files with unapproved licenses:" + System.lineSeparator() + "  /Missing.java", actualText);
+                                        break;
+                                    case XHTML5:
+                                        TextUtils.assertPatternInTarget("<td>Approved<\\/td>\\s+<td>\\d+<\\/td>\\s+<td>A count of approved licenses.<\\/td>", actualText);
+                                        break;
+                                    default:
+                                        fail("No test for stylesheet " + sheet);
+                                        break;
+                                }
+                            } catch (IOException | RatException e) {
+                                throw new RuntimeException(e);
+                            }
+                        }));
+            }
+
+            result.add(new TestData("fileStyleSheet",
+                    Collections.singletonList(ImmutablePair.of(option, new String[]{"fileStyleSheet.xslt"})),
+                    basePath -> {
+                        DefaultLog.getInstance().warn("outputStyleTest setup for " + basePath);
+                        DocumentName name = DocumentName.builder().setName("fileStyleSheet.xslt")
+                                .setBaseName(basePath.toString()).build();
+                        try (FileWriter fileWriter = new FileWriter(new File(name.getBaseName(), name.getName()));
+                             XmlWriter writer = new XmlWriter(fileWriter)) {
+                            writer.startDocument()
+                                    .comment(DataUtils.ASF_TEXT)
+                                    .openElement("xsl:stylesheet")
+                                    .attribute("version", "1.0")
+                                    .attribute("xmlns:xsl", "http://www.w3.org/1999/XSL/Transform")
+                                    .openElement("xsl:template")
+                                    .attribute("match", "@*|node()")
+                                    .content("Hello World")
+                                    .closeDocument();
+                        } catch (IOException e) {
+                            fail(e.getMessage(), e);
+                        }
+                    },
+
+                    validatorData -> {
+                        DefaultLog.getInstance().warn("validating outputStyleTest for " + validatorData.getBaseDir());
+                        try (InputStream expected = StyleSheets.getStyleSheet("fileStyleSheet.xslt", validatorData.getBaseName()).ioSupplier().get();
+                             InputStream actual = validatorData.getConfiguration().getStyleSheet().get()) {
+                            assertThat(IOUtils.contentEquals(expected, actual)).as(() -> "'fileStyleSheet.xslt' does not match").isTrue();
+                            baos.reset();
+                            validatorData.getOutput().format(validatorData.getConfiguration().getStyleSheet(), () -> baos);
+                            String actualText = baos.toString();
+                            TextUtils.assertContainsExactly(1, "Hello World", actualText);
+                        } catch (IOException | RatException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }));
+        }
+    }
+}
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java
index c1fcefdb..84c8c213 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/TestData.java
@@ -21,26 +21,16 @@ package org.apache.rat.testhelpers.data;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.function.Consumer;
-import javax.xml.xpath.XPathExpressionException;
 import org.apache.commons.cli.Option;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.commons.text.WordUtils;
-import org.apache.rat.report.claim.ClaimStatistic;
-import org.apache.rat.testhelpers.XmlUtils;
 import org.apache.rat.ui.ArgumentTracker;
 import org.apache.rat.utils.CasedString;
 import org.apache.rat.utils.FileUtils;
-import org.w3c.dom.Document;
-
-import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * The definition of a test.
@@ -52,7 +42,7 @@ public final class TestData implements Comparable<TestData> {
     private final String name;
     /** The command line for the test */
     private final List<ImmutablePair<Option, String[]>> commandLine;
-    /** A function to setup the test in a specific path */
+    /** A function to set up the test in a specific path */
     private final Consumer<Path> setupFiles;
     /** A function to test the results of the test. */
     private final Consumer<ValidatorData> validator;
@@ -116,8 +106,8 @@ public final class TestData implements Comparable<TestData> {
     }
 
     /**
-     * Gets the expected excepton or {@code null} if not exception is expected.
-     * @return the expected excepton or {@code null} if not exception is expected.
+     * Gets the expected exception or {@code null} if not exception is expected.
+     * @return the expected exception or {@code null} if not exception is expected.
      */
     public Exception getExpectedException() {
         return expectedException;
@@ -176,7 +166,7 @@ public final class TestData implements Comparable<TestData> {
 
     /**
      * Get the arguments for the command line.
-     * @return the argumentsfor the command line as Option, String[] pairs.
+     * @return the arguments for the command line as Option, String[] pairs.
      */
     public List<? extends Pair<Option, String[]>> getArgs() {
         return commandLine;
diff --git a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java
index 55185d66..a4745fe4 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/testhelpers/data/ValidatorData.java
@@ -18,30 +18,13 @@
  */
 package org.apache.rat.testhelpers.data;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import org.apache.rat.ConfigurationException;
 import org.apache.rat.ReportConfiguration;
 import org.apache.rat.Reporter;
-import org.apache.rat.api.RatException;
-import org.apache.rat.configuration.XMLConfig;
-import org.apache.rat.configuration.XMLConfigurationWriter;
 import org.apache.rat.document.DocumentName;
 import org.apache.rat.report.claim.ClaimStatistic;
-import org.apache.rat.report.xml.writer.XmlWriter;
 import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-import static org.apache.rat.configuration.XMLConfigurationReader.nodeListConsumer;
 
 /**
  * The validator for test data.
diff --git a/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/ResourceCollectionContainer.java b/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/ResourceCollectionContainer.java
index df911ad4..b9ddddfd 100644
--- a/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/ResourceCollectionContainer.java
+++ b/apache-rat-tasks/src/main/java/org/apache/rat/anttasks/ResourceCollectionContainer.java
@@ -59,7 +59,7 @@ class ResourceCollectionContainer implements IReportable {
     }
 
     @Override
-    public DocumentName getName() {
+    public DocumentName name() {
         return name;
     }
 }