public class XSpecRunner implements LogProvider {
    public final static String XSPEC_NS = "http://www.jenitennison.com/xslt/xspec";
    public static final String TRACE_SYS_PROP_IGNORE_DIR = "xspec.coverage.ignore";
    public static final String TRACE_SYS_PROP_COVERAGE_FILE = "xspec.coverage.xml";
    public static final String TRACE_SYS_PROP_XSPEC_FILE = "xspec.xspecfile";
    // technical
    private final Log log;
    private final Configuration saxonConfiguration;
    
    // environment
    /**
     * project directory, in a maven use case
     */
    private final File baseDirectory;
    
    // resources
    private XSpecImplResources xspecResources;
    private SchematronImplResources schResources;
    private XSpecPluginResources pluginResources;
    private XmlStuff xmlStuff;
    private final Properties executionProperties;
    private RunnerOptions options;
    
    // internal state management
    private boolean initDone;
    private List<ProcessedFile> processedFiles;
    private XSpecCompiler xspecCompiler;

    public static final QName INITIAL_TEMPLATE_NAME=new QName(XSPEC_NS, "main");
    public static final QName INLINE_CSS = new QName("inline-css");
    private static final String COVERAGE_ERROR_MESSAGE = "Coverage report is only available with Saxon-PE or Saxon-EE";
    private boolean failed;
    
    public XSpecRunner(final Log log, final File baseDirectory) {
        super();
        this.log = log;
        this.baseDirectory = baseDirectory;
        saxonConfiguration = getSaxonConfiguration();
        executionProperties = new Properties();
    }
    
    /**
     * Initalizes the runner. This method must be call before {@link #execute() }
     * @param saxonOptions The saxonOptions to use
     * @return This instance, to chain calls
     * @throws IllegalStateException If {@link #setResources(XSpecImplResources, SchematronImplResources, XSpecPluginResources) }
     * has not been call before.
     * @throws XSpecPluginException If a error occurs during initialization
     */
    public XSpecRunner init(SaxonOptions saxonOptions) throws IllegalStateException, XSpecPluginException {
        if(initDone) {
            throw new IllegalStateException("init(SaxonOptions) has already been call");
        }
        
        if(xspecResources==null || schResources==null || pluginResources==null) {
            throw new IllegalStateException(
                    "setResources(XSpecImplResources,SchematronImplResources,XSpecPluginResources) " +
                    "must be call before init()");
        }
        getLog().debug("Creating XmlStuff...");
        if(options==null) {
            getLog().debug("options was null, creating a new one.");
            options = new RunnerOptions(baseDirectory);
        }
        try {
            xmlStuff = new XmlStuff(
                    new Processor(saxonConfiguration),
                    saxonOptions,
                    getLog(), 
                    xspecResources,
                    pluginResources,
                    schResources,
                    baseDirectory,
                    options,
                    executionProperties
            );
        } catch(XSpecPluginException ex) {
            getLog().error("Exception while creating XmlStuff", ex);
            throw ex;
        }
        xspecCompiler = new XSpecCompiler(xmlStuff, options, log);
        initDone = true;
        return this;
    }
    
    public void execute() throws XSpecPluginException {
        getLog().debug("Looking for XSpecs in: " + options.testDir);
        final List<File> xspecs = findAllXSpecs();
        getLog().info("Found " + xspecs.size() + " XSpecs...");
        failed = false;
        initProcessedFiles(xspecs.size());
        for (final File xspec : xspecs) {
            try {
                if (!processXSpec(xspec)) {
                    failed = true;
                }
            } catch(IOException | TransformerException | SaxonApiException | UncheckedXPathException ex) {
                failed = true;
                getLog().error("while processing "+xspec.getAbsolutePath(), ex);
            }
        }
        
        try {
            extractCssResource();
        } catch(IOException ex) {
            throw new XSpecPluginException("while extracting CSS", ex);
        }
        if (failed) {
            throw new XSpecPluginException("Some XSpec tests failed or were missed!");
        }
    }

    /**
     * Process a XSpec file
     * @param xspec
     * @return <tt>true</tt> if XSpec succeed, <tt>false</tt> otherwise.
     * @throws SaxonApiException
     * @throws TransformerException
     * @throws IOException 
     */
    final boolean processXSpec(final File xspec) throws SaxonApiException, TransformerException, IOException {
        getLog().info("Processing XSpec: " + xspec.getAbsolutePath());

        XdmNode xspecDocument = xmlStuff.getDocumentBuilder().build(xspec);
        XSpecType type = getXSpecType(xspecDocument);
        switch(type) {
            case XQ: return processXQueryXSpec(xspecDocument);
            case SCH: {
                XdmNode compiledSchXSpec = xspecCompiler.prepareSchematronDocument(xspecDocument);
                // it will have a problem in report with filename.
                return processXsltXSpec(compiledSchXSpec);
            }
            default: return processXsltXSpec(xspecDocument);
            
        }
    }
    
    /**
     * Process an XSpec on XQuery Test
     * @param xspec The path to the XSpec test file
     * @return true if all tests in XSpec pass, false otherwise
     */
    final boolean processXQueryXSpec(XdmNode xspec) throws SaxonApiException, FileNotFoundException, IOException {
        File sourceFile = new File(xspec.getBaseURI());
        /* compile the test stylesheet */
        final CompiledXSpec compiledXSpec = xspecCompiler.compileXSpecForXQuery(sourceFile);
        if (compiledXSpec == null) {
            getLog().error("unable to compile "+sourceFile.getAbsolutePath());
            return false;
        } else {
            getLog().debug("XQuery compiled XSpec is at "+compiledXSpec.getCompiledStylesheet().getAbsolutePath());
            /* execute the test stylesheet */
            final XSpecResultsHandler resultsHandler = new XSpecResultsHandler();
            boolean processedFileAdded = false;
            try {
                final XQueryExecutable xeXSpec = xmlStuff.getXqueryCompiler().compile(new FileInputStream(compiledXSpec.getCompiledStylesheet()));
                final XQueryEvaluator xtXSpec = xeXSpec.load();

                getLog().info("Executing XQuery XSpec: " + compiledXSpec.getCompiledStylesheet().getName());

                //setup xml report output
                final File xspecXmlResult = xspecCompiler.getXSpecXmlResultPath(options.reportDir, sourceFile);
                final Serializer xmlSerializer = xmlStuff.getProcessor().newSerializer();
                xmlSerializer.setOutputProperty(Serializer.Property.METHOD, "xml");
                xmlSerializer.setOutputProperty(Serializer.Property.INDENT, "yes");
                xmlSerializer.setOutputFile(xspecXmlResult);

                //setup html report output
                final File xspecHtmlResult = xspecCompiler.getXSpecHtmlResultPath(options.reportDir, sourceFile);
                final Serializer htmlSerializer = xmlStuff.getProcessor().newSerializer();
                htmlSerializer.setOutputProperty(Serializer.Property.METHOD, "html");
                htmlSerializer.setOutputProperty(Serializer.Property.INDENT, "yes");
                htmlSerializer.setOutputFile(xspecHtmlResult);
                XsltTransformer reporter = xmlStuff.getReporter().load();
                reporter.setBaseOutputURI(xspecHtmlResult.toURI().toString());
                reporter.setDestination(htmlSerializer);


                // setup surefire report output
                Destination xtSurefire = null;
                if(xmlStuff.getXeSurefire()!=null) {
                    XsltTransformer xt = xmlStuff.getXeSurefire().load();
                    try {
                        xt.setParameter(new QName("baseDir"), new XdmAtomicValue(baseDirectory.toURI().toURL().toExternalForm()));
                        xt.setParameter(new QName("outputDir"), new XdmAtomicValue(options.surefireReportDir.toURI().toURL().toExternalForm()));
                        xt.setParameter(new QName("reportFileName"), new XdmAtomicValue(xspecXmlResult.getName()));
                        xt.setDestination(xmlStuff.newSerializer(new NullOutputStream()));
                        xtSurefire = xt;
                    } catch(MalformedURLException ex) {
                        getLog().warn("Unable to generate surefire report", ex);
                    }
                } else {
                    xtSurefire = xmlStuff.newSerializer(new NullOutputStream());
                }
                ProcessedFile pf = new ProcessedFile(options.testDir, sourceFile, options.reportDir, xspecHtmlResult);
                processedFiles.add(pf);
                processedFileAdded = true;
                String relativeCssPath = 
                        (pf.getRelativeCssPath().length()>0 ? pf.getRelativeCssPath()+"/" : "") + XmlStuff.RESOURCES_TEST_REPORT_CSS;
                reporter.setParameter(XmlStuff.QN_REPORT_CSS, new XdmAtomicValue(relativeCssPath));
                //execute
                
                final Destination destination = 
                        new TeeDestination(
                                new TeeDestination(
                                        new SAXDestination(resultsHandler), 
                                        new TeeDestination(
                                                xmlSerializer,
                                                xtSurefire)
                                        ),
                                reporter);
                Source xspecSource = new StreamSource(sourceFile);
                xtXSpec.setSource(xspecSource);
                xtXSpec.setURIResolver(xmlStuff.getUriResolver());
                XdmValue result = xtXSpec.evaluate();
                if(result==null) {
                    getLog().debug("processXQueryXSpec result is null");
                } else {
                    getLog().debug("processXQueryXSpec result : "+result.toString());
                    xmlStuff.getProcessor().writeXdmValue(result, destination);
                }
            } catch (final SaxonApiException te) {
                getLog().error(te.getMessage());
                getLog().debug(te);
                if(!processedFileAdded) {
                    ProcessedFile pf = new ProcessedFile(options.testDir, sourceFile, options.reportDir, xspecCompiler.getXSpecHtmlResultPath(options.reportDir, sourceFile));
                    processedFiles.add(pf);
                }
            }
            
            //missed tests come about when the XSLT processor aborts processing the XSpec due to an XSLT error
            final int missed = compiledXSpec.getTests() - resultsHandler.getTests();

            //report results
            final String msg = String.format("%s results [Passed/Pending/Failed/Missed/Total] = [%d/%d/%d/%d/%d]", 
                    sourceFile.getName(), 
                    resultsHandler.getPassed(), 
		    resultsHandler.getPending(),
                    resultsHandler.getFailed(), 
                    missed, 
                    compiledXSpec.getTests());
            if(processedFiles.size()>0) {
                processedFiles.get(processedFiles.size()-1).setResults(
                        resultsHandler.getPassed(), 
                        resultsHandler.getPending(), 
                        resultsHandler.getFailed(), 
                        missed, 
                        compiledXSpec.getTests());
            }
            if (resultsHandler.getFailed() + missed > 0) {
                getLog().error(msg);
                return false;
            } else {
                getLog().info(msg);
                return true;
            }
        }
    }

    /**
     * Process an XSpec on XSLT Test
     * @param xspec The path to the XSpec test file
     * @return true if all tests in XSpec pass, false otherwise
     */
    final boolean processXsltXSpec(XdmNode xspec) throws SaxonApiException, FileNotFoundException {
        File actualSourceFile = new File(xspec.getBaseURI());
        // Try to determine where was the original XSpec file, in case of XSpec on schematron
        File sourceFile = actualSourceFile;
        XPathSelector xps = xmlStuff.getXPathCompiler().compile("/x:description/@xspec-original-location").load();
        xps.setContextItem(xspec);
        XdmItem item = xps.evaluateSingle();
        if(item!=null) {
            String value = item.getStringValue();
            if(!value.isEmpty()) {
                try {
                    sourceFile = new File(new URI(value));
                } catch (URISyntaxException ex) {
                    getLog().error("This should never be possible ! Check /x:description/@xspec-original-location", ex);
                }
            }
        }
        getLog().debug("sourceFile is "+sourceFile.getAbsolutePath());
        
        boolean wasItAnXSpecOnSchematron = !sourceFile.equals(actualSourceFile);
        /* compile the test stylesheet */
        final CompiledXSpec compiledXSpec = xspecCompiler.compileXSpecForXslt(actualSourceFile);
        if (compiledXSpec == null) {
            return false;
        } else {
            getLog().info("XSpec has been compiled");
            /* execute the test stylesheet */
            try {
                final ErrorListener errorListener = new OwnErrorListener(getLog());
                final XsltExecutable xeXSpec = xmlStuff.compileXsl(
                        new StreamSource(compiledXSpec.getCompiledStylesheet()));
                final XsltTransformer xtXSpec = xeXSpec.load();
                xtXSpec.setErrorListener(errorListener);
                
                if(wasItAnXSpecOnSchematron || !options.coverage) {
                    getLog().info("coverage not activated for "+sourceFile.getName());
                    getLog().debug("wasItAnXSpecOnSchematron: "+wasItAnXSpecOnSchematron);
                    getLog().debug("options.coverage: "+options.coverage);
                    return runXsltXspecWithoutCoverage(sourceFile, actualSourceFile, xtXSpec, compiledXSpec, errorListener);
                } else {
                    getLog().info("coverage activated for "+sourceFile.getName());
//                    File coverageFile = xspecCompiler.getCoverageTempPath(options.reportDir, sourceFile);
                    return runXsltXspecWithCoverage(sourceFile, actualSourceFile, xtXSpec, compiledXSpec, errorListener);
                }
            } finally {
                // ben, rien !
            }
        }
    }
    
    private boolean runXsltXspecWithoutCoverage(
            File sourceFile,
            File actualSourceFile,
            XsltTransformer xtXSpec,
            CompiledXSpec compiledXSpec,
            ErrorListener errorListener) throws SaxonApiException, FileNotFoundException {
        boolean processedFileAdded = false;
        final XSpecResultsHandler resultsHandler = new XSpecResultsHandler();
        try {
            xtXSpec.setInitialTemplate(INITIAL_TEMPLATE_NAME);

            getLog().info("Executing XSpec: " + compiledXSpec.getCompiledStylesheet().getName());

            //setup xml report output
            final File xspecXmlResult = xspecCompiler.getXSpecXmlResultPath(options.reportDir, sourceFile);
            final Serializer xmlSerializer = xmlStuff.getProcessor().newSerializer();
            xmlSerializer.setOutputProperty(Serializer.Property.METHOD, "xml");
            xmlSerializer.setOutputProperty(Serializer.Property.INDENT, "yes");
            xmlSerializer.setOutputFile(xspecXmlResult);
            getLog().debug("\txml report output set");

            //setup html report output
            final File xspecHtmlResult = xspecCompiler.getXSpecHtmlResultPath(options.reportDir, sourceFile);
            final Serializer htmlSerializer = xmlStuff.getProcessor().newSerializer();
            htmlSerializer.setOutputProperty(Serializer.Property.METHOD, "html");
            htmlSerializer.setOutputProperty(Serializer.Property.INDENT, "yes");
            htmlSerializer.setOutputFile(xspecHtmlResult);
            XsltTransformer reporter = xmlStuff.getReporter().load();
            reporter.setErrorListener(errorListener);
            reporter.setBaseOutputURI(xspecHtmlResult.toURI().toString());
            reporter.setDestination(htmlSerializer);
            getLog().debug("\thtml report output set");


            // setup surefire report output
            Destination xtSurefire = null;
            if(xmlStuff.getXeSurefire()!=null) {
                XsltTransformer xt = xmlStuff.getXeSurefire().load();
                xt.setErrorListener(errorListener);
                try {
                    xt.setParameter(new QName("baseDir"), new XdmAtomicValue(baseDirectory.toURI().toURL().toExternalForm()));
                    // issue #40
                    xt.setParameter(new QName("outputDir"), new XdmAtomicValue(options.surefireReportDir.toURI().toURL().toExternalForm()));
                    xt.setParameter(new QName("reportFileName"), new XdmAtomicValue(xspecXmlResult.getName()));
                    xt.setDestination(xmlStuff.newSerializer(new NullOutputStream()));
                    xtSurefire = xt;
                } catch(MalformedURLException ex) {
                    getLog().warn("Unable to generate surefire report", ex);
                }
            } else {
                xtSurefire = xmlStuff.newSerializer(new NullOutputStream());
            }
            getLog().debug("\tsurefire report output set");

            getLog().debug("\tcreating PF");
            ProcessedFile pf = new ProcessedFile(options.testDir, sourceFile, options.reportDir, xspecHtmlResult);
            getLog().debug("\tadding PF to list");
            processedFiles.add(pf);
            processedFileAdded = true;
            getLog().debug("\tprocessedFile processed");
            String relativeCssPath = 
                    (pf.getRelativeCssPath().length()>0 ? pf.getRelativeCssPath()+"/" : "") + XmlStuff.RESOURCES_TEST_REPORT_CSS;
            getLog().debug("\trelativeCssPath: "+relativeCssPath);
            // issue #36
            reporter.setParameter(XmlStuff.QN_REPORT_CSS, new XdmAtomicValue(relativeCssPath));

            //execute
            final Destination destination = 
                    new TeeDestination(
                            new TeeDestination(
                                    new SAXDestination(resultsHandler), 
                                    new TeeDestination(
                                            xmlSerializer,
                                            xtSurefire)
                                    ), 
                            reporter);
            getLog().debug("\tdestination tree constructed");

            XMLReader reader = xmlStuff.PARSER_FACTORY.newSAXParser().getXMLReader();
            reader.setEntityResolver((EntityResolver)xmlStuff.getUriResolver());
            Source xspecSource = new SAXSource(reader, new InputSource(new FileInputStream(sourceFile)));
            xspecSource.setSystemId(sourceFile.toURI().toString());
            xtXSpec.setSource(xspecSource);
            xtXSpec.setURIResolver(xmlStuff.getUriResolver());
            xtXSpec.setDestination(destination);
            xtXSpec.setBaseOutputURI(xspecXmlResult.toURI().toString());
            getLog().debug("\tlaunching transform");
            xtXSpec.transform();

            getLog().debug("XSpec run");
        } catch (final SaxonApiException te) {
            getLog().error(te.getMessage());
            getLog().debug(te);
            if(!processedFileAdded) {
                ProcessedFile pf = new ProcessedFile(options.testDir, sourceFile, options.reportDir, xspecCompiler.getXSpecHtmlResultPath(options.reportDir, sourceFile));
                processedFiles.add(pf);
            }
        } catch (final FileNotFoundException | ParserConfigurationException | SAXException te) {
            getLog().error(te.getMessage());
            getLog().debug(te);
        }

        //missed tests come about when the XSLT processor aborts processing the XSpec due to an XSLT error
        final int missed = compiledXSpec.getTests() - resultsHandler.getTests();

        //report results
        final String msg = String.format("%s results [Passed/Pending/Failed/Missed/Total] = [%d/%d/%d/%d/%d]", 
                sourceFile.getName(), 
                resultsHandler.getPassed(), 
                resultsHandler.getPending(),
                resultsHandler.getFailed(), 
                missed, 
                compiledXSpec.getTests());
        if(processedFiles.size()>0) {
            processedFiles.get(processedFiles.size()-1).setResults(
                    resultsHandler.getPassed(), 
                    resultsHandler.getPending(), 
                    resultsHandler.getFailed(), 
                    missed, 
                    compiledXSpec.getTests());
        }
        if (resultsHandler.getFailed() + missed > 0) {
            getLog().error(msg);
            return false;
        } else {
            getLog().info(msg);
            return true;
        }
    }
    private boolean runXsltXspecWithCoverage(
            File sourceFile,
            File actualSourceFile,
            XsltTransformer xtXSpec,
            CompiledXSpec compiledXSpec,
            ErrorListener errorListener) throws SaxonApiException, FileNotFoundException {
        boolean processedFileAdded = false;
        final XSpecResultsHandler resultsHandler = new XSpecResultsHandler();
        try {
            File coverageFile = xspecCompiler.getCoverageTempPath(options.reportDir, sourceFile);
            getLog().debug("coverage File: "+coverageFile.getAbsolutePath());
            // WARNING : as System properties are used, a multi-threaded system can not be used !
            System.setProperty(TRACE_SYS_PROP_IGNORE_DIR, compiledXSpec.getCompiledStylesheet().getParentFile().getAbsolutePath());
            System.setProperty(TRACE_SYS_PROP_XSPEC_FILE, sourceFile.getAbsolutePath());
            System.setProperty(TRACE_SYS_PROP_COVERAGE_FILE, coverageFile.getAbsolutePath());
//            File tempCoverageFile = xspecCompiler.getCoverageTempPath(options.reportDir, sourceFile);
            try {
                // need to set system properties
                TraceListener tl =  new XSLTCoverageTraceListener();
                xtXSpec.setTraceListener(tl);
                getLog().info("Trace listener is active");
            } catch(Exception ex) {
                getLog().error("while instanciating XSLTCoverageTraceListener", ex);
            }
            xtXSpec.setInitialTemplate(INITIAL_TEMPLATE_NAME);

            getLog().info("Executing XSpec: " + compiledXSpec.getCompiledStylesheet().getName());
            final File xspecXmlResult = xspecCompiler.getXSpecXmlResultPath(options.reportDir, sourceFile);

            XMLReader reader = xmlStuff.PARSER_FACTORY.newSAXParser().getXMLReader();
            reader.setEntityResolver((EntityResolver)xmlStuff.getUriResolver());
            Source xspecSource = new SAXSource(reader, new InputSource(new FileInputStream(sourceFile)));
            xspecSource.setSystemId(sourceFile.toURI().toString());
            xtXSpec.setSource(xspecSource);
            xtXSpec.setURIResolver(xmlStuff.getUriResolver());
            XdmDestination xspecResult = new XdmDestination();
            xtXSpec.setDestination(xspecResult);
            xtXSpec.setBaseOutputURI(xspecXmlResult.toURI().toString());
            getLog().debug("\tlaunching transform");
            xtXSpec.transform();
            getLog().debug("XSpec run");


            //setup xml report output
            final Serializer xmlSerializer = xmlStuff.getProcessor().newSerializer();
            xmlSerializer.setOutputProperty(Serializer.Property.METHOD, "xml");
            xmlSerializer.setOutputProperty(Serializer.Property.INDENT, "yes");
            xmlSerializer.setOutputFile(xspecXmlResult);
            getLog().debug("\txml report output set");

            //setup html report output
            final File xspecHtmlResult = xspecCompiler.getXSpecHtmlResultPath(options.reportDir, sourceFile);
            final Serializer htmlSerializer = xmlStuff.getProcessor().newSerializer();
            htmlSerializer.setOutputProperty(Serializer.Property.METHOD, "html");
            htmlSerializer.setOutputProperty(Serializer.Property.INDENT, "yes");
            htmlSerializer.setOutputFile(xspecHtmlResult);
            XsltTransformer reporter = xmlStuff.getReporter().load();
            reporter.setErrorListener(errorListener);
            reporter.setBaseOutputURI(xspecHtmlResult.toURI().toString());
            reporter.setDestination(htmlSerializer);
            getLog().debug("\thtml report output set");


            // setup surefire report output
            Destination xtSurefire = null;
            if(xmlStuff.getXeSurefire()!=null) {
                XsltTransformer xt = xmlStuff.getXeSurefire().load();
                xt.setErrorListener(errorListener);
                try {
                    xt.setParameter(new QName("baseDir"), new XdmAtomicValue(baseDirectory.toURI().toURL().toExternalForm()));
                    // issue #40
                    xt.setParameter(new QName("outputDir"), new XdmAtomicValue(options.surefireReportDir.toURI().toURL().toExternalForm()));
                    xt.setParameter(new QName("reportFileName"), new XdmAtomicValue(xspecXmlResult.getName()));
                    xt.setDestination(xmlStuff.newSerializer(new NullOutputStream()));
                    xtSurefire = xt;
                } catch(MalformedURLException ex) {
                    getLog().warn("Unable to generate surefire report", ex);
                }
            } else {
                xtSurefire = xmlStuff.newSerializer(new NullOutputStream());
            }
            getLog().debug("\tsurefire report output set");

            getLog().debug("\tcreating PF");
            ProcessedFile pf = new ProcessedFile(options.testDir, sourceFile, options.reportDir, xspecHtmlResult);
            getLog().debug("\tadding PF to list");
            processedFiles.add(pf);
            processedFileAdded = true;
            getLog().debug("\tprocessedFile processed");
            String relativeCssPath = 
                    (pf.getRelativeCssPath().length()>0 ? pf.getRelativeCssPath()+"/" : "") + XmlStuff.RESOURCES_TEST_REPORT_CSS;
            getLog().debug("\trelativeCssPath: "+relativeCssPath);
            reporter.setParameter(XmlStuff.QN_REPORT_CSS, new XdmAtomicValue(relativeCssPath));

            //execute
            final Destination destination = 
                    new TeeDestination(
                            new TeeDestination(
                                    new SAXDestination(resultsHandler), 
                                    new TeeDestination(
                                            xmlSerializer,
                                            xtSurefire)
                                    ), 
                            reporter);
            getLog().debug("\tdestination tree constructed");

            // here, we process the XSpec result through all the destinations
            xmlStuff.getProcessor().writeXdmValue(xspecResult.getXdmNode(), destination);

            // coverage
            if(xmlStuff.getCoverageReporter()!=null) {
                XsltTransformer coverage = xmlStuff.getCoverageReporter().load();
                coverage.setErrorListener(errorListener);
                File coverageReportFile = xspecCompiler.getCoverageFinalPath(options.reportDir, sourceFile);
                pf.setCoverageFile(coverageReportFile.toPath());
                coverage.setDestination(xmlStuff.getProcessor().newSerializer(coverageReportFile));
//                coverage.setInitialContextNode(xspecResult.getXdmNode());
                coverage.setSource(new StreamSource(coverageFile));
//                getLog().info("coverage pwd: "+options.testDir.toURI().toString());
//                coverage.setParameter(new QName("pwd"),XdmAtomicValue.makeAtomicValue(options.testDir.toURI().toString()));
//                Path relative = options.testDir.toPath().relativize(sourceFile.toPath());
//                getLog().info("coverage tests: "+relative.toString());
//                coverage.setParameter(new QName("tests"), XdmAtomicValue.makeAtomicValue(relative.toString()));
                coverage.setParameter(INLINE_CSS, XdmAtomicValue.makeAtomicValue("false"));
                coverage.setParameter(XmlStuff.QN_REPORT_CSS, new XdmAtomicValue(relativeCssPath));
                coverage.transform();
            } else {
                getLog().warn(COVERAGE_ERROR_MESSAGE);
            }

        } catch (final SaxonApiException te) {
            getLog().error(te.getMessage());
            getLog().debug(te);
            if(!processedFileAdded) {
                ProcessedFile pf = new ProcessedFile(options.testDir, sourceFile, options.reportDir, xspecCompiler.getXSpecHtmlResultPath(options.reportDir, sourceFile));
                processedFiles.add(pf);
            }
        } catch (final FileNotFoundException | ParserConfigurationException | SAXException te) {
            getLog().error(te.getMessage());
            getLog().debug(te);
        }

        //missed tests come about when the XSLT processor aborts processing the XSpec due to an XSLT error
        final int missed = compiledXSpec.getTests() - resultsHandler.getTests();

        //report results
        final String msg = String.format("%s results [Passed/Pending/Failed/Missed/Total] = [%d/%d/%d/%d/%d]", 
                sourceFile.getName(), 
                resultsHandler.getPassed(), 
                resultsHandler.getPending(),
                resultsHandler.getFailed(), 
                missed, 
                compiledXSpec.getTests());
        if(processedFiles.size()>0) {
            processedFiles.get(processedFiles.size()-1).setResults(
                    resultsHandler.getPassed(), 
                    resultsHandler.getPending(), 
                    resultsHandler.getFailed(), 
                    missed, 
                    compiledXSpec.getTests());
        }
        if (resultsHandler.getFailed() + missed > 0) {
            getLog().error(msg);
            getLog().debug("\tXSpec terminated, return false");
            return false;
        } else {
            getLog().info(msg);
            getLog().debug("\tXSpec terminated, return true");
            return true;
        }
    }
    /**
     * Returns the XSpec tested file kind
     * @param doc
     * @return XSpecType
     * @throws SaxonApiException If file is not a XSpec one
     */
    XSpecType getXSpecType(XdmNode doc) throws SaxonApiException {
        XPathSelector xps = xmlStuff.getXpExecGetXSpecType().load();
        xps.setContextItem(doc);
        XdmValue values = xps.evaluate();
        Object o = values.iterator();
        if(o instanceof XdmSequenceIterator) {
            for(XdmSequenceIterator it=(XdmSequenceIterator)o; it.hasNext();) {
                XdmNode item=(XdmNode)(it.next());
                if(item.getNodeKind().equals(XdmNodeKind.ATTRIBUTE)) {
                    String nodeName = item.getNodeName().getLocalName();
                    switch(nodeName) {
                        case "query": 
                        case "query-at": {
                            return XSpecType.XQ;
                        }
                        case "schematron": {
                            return XSpecType.SCH;
                        }
                        case "stylesheet": {
                            return XSpecType.XSL;
                        }
                    }
                }
            }
        } else {
            for(Iterator<XdmItem> it=(Iterator<XdmItem>)o; it.hasNext();) {
                XdmNode item=(XdmNode)(it.next());
                if(item.getNodeKind().equals(XdmNodeKind.ATTRIBUTE)) {
                    String nodeName = item.getNodeName().getLocalName();
                    switch(nodeName) {
                        case "query": 
                        case "query-at": {
                            return XSpecType.XQ;
                        }
                        case "schematron": {
                            return XSpecType.SCH;
                        }
                        case "stylesheet": {
                            return XSpecType.XSL;
                        }
                    }
                }
            }
        }
        throw new SaxonApiException("This file does not seem to be a valid XSpec file: "+doc.getBaseURI().toString());
    }

    /**
     * Set the implementation resources to use. <b>Must be call before {@link #init(SaxonOptions) }</b>.
     * @param xspecResources XSpec implementation resources
     * @param schResources Schematron implementation resource
     * @param pluginResources Plugin-specific implementation resources
     * @return This instance, to chain calls
     */
    public XSpecRunner setResources(
            XSpecImplResources xspecResources,
            SchematronImplResources schResources,
            XSpecPluginResources pluginResources) {
        this.xspecResources = xspecResources;
        this.schResources = schResources;
        this.pluginResources = pluginResources;
        return this;
    }
    
    /**
     * Defines environment. Mainly, <tt>session.getUserProperties()</tt>
     * and <tt>session.getSystemProperties()</tt>
     * @param executionProperties The properties provided by maven execution.
     * @param options Execution options (usually, all Mojo parameters)
     * @return 
     */
    public XSpecRunner setEnvironment(Properties executionProperties, RunnerOptions options) {
        this.executionProperties.putAll(executionProperties);
        this.options = options;
        return this;
    }
    
    /**
     * Generates general index. May be overriden if required
     * @throws XSpecPluginException 
     */
    public void generateIndex() throws XSpecPluginException {
        if(processedFiles==null) {
            throw new IllegalStateException("no execution has been done. processedFiles is null");
        }
        getLog().debug("processedFiles is "+processedFiles.size()+" length");
        IndexGenerator generator = new IndexGenerator(options, processedFiles, xmlStuff);
        generator.generateIndex();
    }
        
    /**
     * We want to be sure that external-functions are allowed
     * @return 
     */
    private static Configuration getSaxonConfiguration() {
        Configuration ret = Configuration.newConfiguration();
        ret.setConfigurationProperty("http://saxon.sf.net/feature/allow-external-functions", Boolean.TRUE);
        return ret;
    }
    
    /**
     * Package private to allow unit tests
     */
    List<File> findAllXSpecs() throws XSpecPluginException {
        if (!options.testDir.exists()) {
            return Collections.emptyList();
        }
        FileFinder finder = new FileFinder(options.testDir, "**/*.xspec", options.excludes, getLog());
        final Path testPath = options.testDir.toPath();
        try {
            return finder.search().stream()
                    .map(p -> testPath.resolve(p).toFile())
                    .collect(Collectors.toList());
        } catch(IOException ex) {
            throw new XSpecPluginException(ex);
        }
    }
    
    @Override
    public Log getLog() { return log; }
    
    protected void extractCssResource() throws MalformedURLException, IOException {
        File cssFile = new File(options.reportDir, XmlStuff.RESOURCES_TEST_REPORT_CSS);
        cssFile.getParentFile().mkdirs();
        try {
            Source cssSource = xmlStuff.getUriResolver().resolve(xspecResources.getXSpecCssReportUri(), baseDirectory.toURI().toURL().toExternalForm());
            BufferedInputStream is = new BufferedInputStream(new URL(cssSource.getSystemId()).openStream());
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(cssFile));
            byte[] buffer = new byte[1024];
            int read = is.read(buffer);
            while(read>0) {
                bos.write(buffer, 0, read);
                read = is.read(buffer);
            }
            is.close();
            bos.close();
        } catch(TransformerException ex) {
            getLog().error("while extracting CSS: ",ex);
        }
    }
    
    // for UT only
    XmlStuff getXmlStuff() { return xmlStuff; }

    /**
     * expose this to package to let unit tests initialize PF, 
     * when running outside of {@link #execute()} method.
     */
    void initProcessedFiles(int size) {
        processedFiles= new ArrayList<>(size);
    }
    
} public interface SchematronImplResources {
    
    /**
     * Usually iso_dsdl_include.xsl
     * @return Schematron ISO dsdl include URI
     */
    public String getSchIsoDsdlIncludeUri();
    
    /**
     * Usually iso_abstract_expand.xsl
     * @return Schematron ISO abstract expand URI
     */
    public String getSchIsoAbstractExpandUri();
    
    /**
     * Usuall iso_svrl_for_xslt2.xsl
     * @return Schematron ISO Svrl for XSLT includes URI
     */
    public String getSchIsoSvrlForXslt2Uri();
    
} public interface XSpecImplResources {
    public static final String XSPEC_PREFIX = "cp:/";

    /**
     * Usually, generate-xspec-tests.xsl
     * @return XSpec compiler for XSL URI
     */
    public String getXSpecXslCompilerUri();
    
    /**
     * Usually generate-query-tests.xsl
     * @return XSpec compiler for XQuery URI
     */
    public String getXSpecXQueryCompilerUri();
    
    /**
     * Usually schut-to-xspec.xsl
     * @return XSpec-for-Schematron converter URI
     */
    public String getSchematronSchutConverterUri();
    
    /**
     * Usually format-xspec-report.xsl
     * @param useFolding
     * @return XSpec reporter URI
     */
    public String getXSpecReporterUri(boolean useFolding);
    
    /**
     * Usually coverage-report.xsl
     * @return XSpec Code Coverage reporter URI
     */
    public String getXSpecCoverageReporterUri();
    
    /**
     * Usually test-report.css
     * @return 
     */
    public String getXSpecCssReportUri();
    
} public interface XSpecPluginResources {
    public static final String XML_UTILITIES_PREFIX = "cp:/";
    public static final String LOCAL_PREFIX = "cp:/";
    public static final String CATALOG_NS = "urn:oasis:names:tc:entity:xmlns:xml:catalog";
    
    /**
     * Returns URI for dependency scanner utility
     * @return Dependency scanner URI
     */
    public String getDependencyScannerUri();
    /**
     * Image Down for folding report
     * @return 
     */
    public String getImageDown();
    
    /**
     * Image Up for folding report
     * @return 
     */
    public String getImageUp();
    
    /**
     * XSL that changes external images to inline images in folding report
     * @return 
     */
    public String getXsltImageChanger();
} public class DefaultSchematronImplResources implements SchematronImplResources {
    public static final transient String SCHEMATRON_PREFIX = "cp:/";
    private String schIsoDsdlInclude;
    private String schIsoAbstractExpand;
    private String schIsoSvrlForXslt2;
    
    public DefaultSchematronImplResources() {
        super();
        this.schIsoDsdlInclude = SCHEMATRON_PREFIX+"schematron/iso-schematron/iso_dsdl_include.xsl";
        this.schIsoAbstractExpand = SCHEMATRON_PREFIX+"schematron/iso-schematron/iso_abstract_expand.xsl";
        this.schIsoSvrlForXslt2 = SCHEMATRON_PREFIX+"schematron/iso-schematron/iso_svrl_for_xslt2.xsl";
    }

    @Override
    public String getSchIsoDsdlIncludeUri() {
        return schIsoDsdlInclude;
    }

    public void setSchIsoDsdlInclude(String schIsoDsdlInclude) {
        this.schIsoDsdlInclude = schIsoDsdlInclude;
    }

    @Override
    public String getSchIsoAbstractExpandUri() {
        return schIsoAbstractExpand;
    }

    public void setSchIsoAbstractExpand(String schIsoAbstractExpand) {
        this.schIsoAbstractExpand = schIsoAbstractExpand;
    }

    @Override
    public String getSchIsoSvrlForXslt2Uri() {
        return schIsoSvrlForXslt2;
    }

    public void setSchIsoSvrlForXslt2(String schIsoSvrlForXslt2) {
        this.schIsoSvrlForXslt2 = schIsoSvrlForXslt2;
    }
    
} public class DefaultXSpecImplResources implements XSpecImplResources {

    private String xspecXslCompilerUri;
    private String xspecXQueryCompilerUri;
    private String schematronUnitTestConverter;
    private String xspecReporter;
    private final String xspecFoldingReporter;
    private String xspecCoverageReporter;
    private String testReportCss;
    
    public DefaultXSpecImplResources() {
        super();
        xspecXslCompilerUri = XSPEC_PREFIX+"compiler/generate-xspec-tests.xsl";
        xspecXQueryCompilerUri = XSPEC_PREFIX+"compiler/generate-query-tests.xsl";
        schematronUnitTestConverter = XSPEC_PREFIX+"schematron/schut-to-xspec.xsl";
        xspecReporter = XSPEC_PREFIX+"reporter/format-xspec-report.xsl";
        xspecFoldingReporter = XSPEC_PREFIX+"reporter/format-xspec-report-folding.xsl";
        xspecCoverageReporter = XSPEC_PREFIX+"reporter/coverage-report.xsl";
        testReportCss = XSPEC_PREFIX+"reporter/test-report.css";
    }

    @Override
    public String getXSpecXslCompilerUri() { return xspecXslCompilerUri; }

    @Override
    public String getXSpecXQueryCompilerUri() { return xspecXQueryCompilerUri; }

    @Override
    public String getSchematronSchutConverterUri() { return schematronUnitTestConverter; }

    @Override
    public String getXSpecReporterUri(boolean useFolding) { 
        if(useFolding) {
            return xspecFoldingReporter;
        }
        return xspecReporter;
    }

    @Override
    public String getXSpecCoverageReporterUri() { return xspecCoverageReporter; }

    @Override
    public String getXSpecCssReportUri() { return testReportCss; }

    /**
     * Defines the XSpec compiler for XSL URI
     * @param xspecXslCompilerUri 
     */
    public void setXSpecXslCompilerUri(String xspecXslCompilerUri) {
        this.xspecXslCompilerUri=xspecXslCompilerUri;
    }
    
    /**
     * Defines the XSpec compiler for XQuery URI
     * @param xspecXQueryCompilerUri 
     */
    public void setXSpecXQueryCompiler(String xspecXQueryCompilerUri) {
        this.xspecXQueryCompilerUri = xspecXQueryCompilerUri;
    }
    
    /**
     * Defines the XSpec-for-Schematron converter URI
     * @param schematronUnitTestConverter 
     */
    public void setSchematronSchutConverter(String schematronUnitTestConverter) {
        this.schematronUnitTestConverter = schematronUnitTestConverter;
    }

    /**
     * Defines the XSpec reporter URI
     * @param xspecReporter 
     */
    public void setXSpecReporter(String xspecReporter) {
        this.xspecReporter = xspecReporter;
    }

    /**
     * Defines the XSpec Code Coverage reporter URI
     * @param xspecCoverageReporter 
     */
    public void setXSpecCoverageReporter(String xspecCoverageReporter) {
        this.xspecCoverageReporter = xspecCoverageReporter;
    }

    /**
     * Defines the report Css URI
     * @param testReportCss 
     */
    public void setTestReportCss(String testReportCss) {
        this.testReportCss = testReportCss;
    }

} public class DefaultXSpecPluginResources implements XSpecPluginResources {
    
    private String dependencyScanner;
    private final String imageDown;
    private final String imageUp;
    private final String imageChangerXsl;
    
    public DefaultXSpecPluginResources() {
        super();
        this.dependencyScanner = XML_UTILITIES_PREFIX+"org/mricaud/xml-utilities/get-xml-file-static-dependency-tree.xsl";
        this.imageDown = LOCAL_PREFIX+"reporter/3angle-down.gif";
        this.imageUp = LOCAL_PREFIX+"reporter/3angle-right.gif";
        this.imageChangerXsl = LOCAL_PREFIX+"io/xspec/maven/xspec-maven-plugin/reporter/folding-reporter-inline-image.xsl";
    }

    @Override
    public String getDependencyScannerUri() {
        return dependencyScanner;
    }

    public void setDependencyScanner(String dependencyScanner) {
        this.dependencyScanner = dependencyScanner;
    }

    @Override
    public String getImageDown() {
        return imageDown;
    }

    @Override
    public String getImageUp() {
        return imageUp;
    }

    @Override
    public String getXsltImageChanger() {
        return imageChangerXsl;
    }

} public interface LogProvider {
    Log getLog();
} public class RunnerOptions {
    /**
     * Says if generated catalog must be kept, or deleted at the end of execution
     */
    public Boolean keepGeneratedCatalog = Boolean.FALSE;

    /**
     * The provided catalog to use (to add to generated catalog).
     * It is a String, as it can contains properties that will be resolved
     * with environment properties.
     */
    public String catalogFile;
    
    /**
     * The folder where are located xspec files
     */
    public File testDir;
    
    public List<String> excludes;
    
    public File reportDir;
    
    public String executionId;

    public File surefireReportDir;
    
    public Boolean coverage;
    
    public Boolean folding;
    

    
    /**
     * Construct a new RunnerOptions instance, with all default values,
     * according to maven defaults
     * @param baseDir Project base directory
     */
    public RunnerOptions(File baseDir) {
        super();
        testDir = new File(baseDir, "src/test/xspec");
        reportDir = new File(baseDir, "target/xspec-reports");
        excludes = new ArrayList<>();
        executionId = "default";
        surefireReportDir = new File(baseDir, "target/surefire-reports");
        coverage = Boolean.FALSE;
        folding = Boolean.FALSE;
    }
    
    /**
     * Creates a RunnerOptions, with specified values;
     * @param baseDir Project base directory
     * @param keepGeneratedCatalog keep or not generated catalog
     * @param catalogFile The catalogFile to use
     * @param excludes The exclude patterns
     * @param testDir The directory where XSpec are stored in
     * @param reportDir The directory where to put reports
     * @param executionId ExecutionId to use;
     * @param surefireReportDir Surefire report dir
     * @param coverage Says if coverage should be process or not
     * @param folding Says if folding report should be use
     */
    public RunnerOptions(
            File baseDir, 
            Boolean keepGeneratedCatalog, 
            String catalogFile, 
            List<String> excludes, 
            File testDir, 
            File reportDir,
            String executionId,
            File surefireReportDir,
            Boolean coverage,
            Boolean folding) {
        this(baseDir);
        if(keepGeneratedCatalog!=null) {
            this.keepGeneratedCatalog = keepGeneratedCatalog;
        }
        if(catalogFile!=null) {
            this.catalogFile = catalogFile;
        }
        if(excludes!=null) {
            this.excludes.addAll(excludes);
        }
        if(testDir!=null) {
            this.testDir = testDir;
        }
        if(reportDir!=null) {
            this.reportDir = reportDir;
        }
        if(executionId!=null) {
            this.executionId = executionId;
        }
        if(surefireReportDir!=null) {
            this.surefireReportDir = surefireReportDir;
        }
        if(coverage!=null) {
            this.coverage = coverage;
        }
        if(folding!=null) {
            this.folding = folding;
        }
    }
    
} public class XSpecPluginException extends Exception {

    public XSpecPluginException() {
    }

    public XSpecPluginException(String message) {
        super(message);
    }

    public XSpecPluginException(String message, Throwable cause) {
        super(message, cause);
    }

    public XSpecPluginException(Throwable cause) {
        super(cause);
    }

    public XSpecPluginException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
    
} public class XmlStuff {
    private final Processor processor;
    private final DocumentBuilder documentBuilder;
    private final XsltCompiler xsltCompiler;
    private final XQueryCompiler xqueryCompiler;
    private final XPathCompiler xpathCompiler;
    
    private XsltExecutable xspec4xsltCompiler;
    private XsltExecutable xspec4xqueryCompiler;
    private XsltExecutable reporter;
    private XsltExecutable junitReporter;
    private XsltExecutable coverageReporter;
    private XsltExecutable xeSurefire;
    private XsltExecutable schDsdl;
    private XsltExecutable schExpand;
    private XsltExecutable schSvrl;
    private XsltExecutable xmlDependencyScanner;
    private XPathExecutable xpExecGetXSpecType;
    private XPathExecutable xpFileSearcher;

    public final static QName QN_REPORT_CSS = new QName("report-css-uri");
    public static final String RESOURCES_TEST_REPORT_CSS = "resources/test-report.css";
    private XPathExecutable xpSchGetXSpec;
    private XsltExecutable schSchut;
    private final Log log;
    private final XSpecImplResources xspecResources;
    private final XSpecPluginResources pluginResources;
    private final SchematronImplResources schematronResources;
    private final File baseDir;
    private final RunnerOptions options;
    private final Properties executionProperties;
    // for code coverage
    private String generateXspecUtilsUri;
    private String schLocationCompareUri;
    public static final SAXParserFactory PARSER_FACTORY = SAXParserFactory.newInstance();
    private static final Class[] EMPTY_PARAMS = new Class[]{};

    
    public XmlStuff(
            Processor processor,
            SaxonOptions saxonOptions,
            Log log, 
            XSpecImplResources xspecResources, 
            XSpecPluginResources pluginResources, 
            SchematronImplResources schematronResources,
            File baseDir,
            RunnerOptions options,
            Properties executionProperties) throws XSpecPluginException {
        super();
        this.processor = processor;
        this.xspecResources = xspecResources;
        this.pluginResources = pluginResources;
        this.schematronResources = schematronResources;
        this.baseDir = baseDir;
        this.options=options;
        this.executionProperties=executionProperties;
        this.log=log;
        if(saxonOptions!=null) {
            try {
                SaxonUtils.prepareSaxonConfiguration(this.processor, saxonOptions);
            } catch(XPathException ex) {
                getLog().error(ex);
                throw new XSpecPluginException("Illegal value in Saxon configuration property", ex);
            }
        }
        documentBuilder = processor.newDocumentBuilder();
        try {
            xsltCompiler = processor.newXsltCompiler();
            xsltCompiler.setCompileWithTracing(true);
            xpathCompiler = processor.newXPathCompiler();
            xpathCompiler.declareNamespace("x", XSpecMojo.XSPEC_NS);
            xqueryCompiler = processor.newXQueryCompiler();
            xqueryCompiler.setCompileWithTracing(true);
            try {
                doAdditionalConfiguration(saxonOptions);
            } catch(XPathException ex) {
                throw new XSpecPluginException(ex);
            }
            try {
                xsltCompiler.setURIResolver(buildUriResolver(xsltCompiler.getURIResolver()));
            } catch(IOException ex) {
                throw new XSpecPluginException("while constructing URIResolver", ex);
            }
            getLog().debug("URI resolver Ok");
            loadAllSaxonExtensionFunctions();
            try {
                createXPathExecutables();
                getLog().debug("XPath executables created");
                createXsltExecutables();
                getLog().debug("XSLT executables created");
            } catch(XSpecPluginException | SaxonApiException | URISyntaxException | IOException ex) {
                throw new XSpecPluginException(ex);
            }
        } catch(RuntimeException ex) {
            throw new XSpecPluginException(ex.getMessage(), ex);
        }
    }

    private void loadAllSaxonExtensionFunctions() {
        try {
            for(Enumeration<URL> enumer = getClass().getClassLoader().getResources("META-INF/services/top.marchand.xml.gaulois.xml"); enumer.hasMoreElements();) {
                URL url = enumer.nextElement();
                log.debug("loading service "+url.toExternalForm());
                XdmNode document = documentBuilder.build(new StreamSource(url.openStream()));
                XPathSelector selector = xpathCompiler.compile("/gaulois-services/saxon/extensions/function").load();
                selector.setContextItem(document);
                Object o = selector.evaluate().iterator();
                if(o instanceof XdmSequenceIterator) {
                    // saxon 9.8
                    XdmSequenceIterator it = (XdmSequenceIterator)o;
                    while(it.hasNext()) {
                        String className = it.next().getStringValue();
                        registerSaxonExtension(className);
                    }
                } else {
                    Iterator<XdmItem> it = (Iterator<XdmItem>)o;
                    while(it.hasNext()) {
                        String className = it.next().getStringValue();
                        registerSaxonExtension(className);
                    }
                }
            }
        } catch(IOException | SaxonApiException ex) {
            getLog().error("while looking for resources in /META-INF/services/top.marchand.xml.gaulois/", ex);
        }
        registerSaxonExtension("io.xspec.xspec.saxon.funcdefs.LineNumber");
    }

    private void registerSaxonExtension(String className) {
        try {
            Class clazz = Class.forName(className);
            if(extendsClass(clazz, ExtensionFunctionDefinition.class)) {
                Class<ExtensionFunctionDefinition> cle = (Class<ExtensionFunctionDefinition>)clazz;
                Constructor<ExtensionFunctionDefinition> cc = cle.getConstructor(EMPTY_PARAMS);
                processor.getUnderlyingConfiguration().registerExtensionFunction(cc.newInstance());
                log.debug(className+"registered as Saxon extension function");
            } else {
                log.warn(className+" does not extends "+ExtensionFunctionDefinition.class.getName());
            }
        } catch(
                ClassNotFoundException | 
                InstantiationException | 
                IllegalAccessException | 
                InvocationTargetException |
                NoSuchMethodException ex) {
            log.warn("unable to load extension function "+className);
        }
    }
    
    /**
     * Creates the URI Resolver. It also generates the catalog with all
     * dependencies
     * @param saxonUriResolver
     * @return
     * @throws IOException
     * @throws XSpecPluginException
     */
    private URIResolver buildUriResolver(final URIResolver saxonUriResolver) throws IOException, XSpecPluginException {
        getLog().debug("buildUriResolver");
        CatalogWriter cw = new CatalogWriter(this.getClass().getClassLoader());
        getLog().debug("CatalogWriter instanciated");
        File catalog = cw.writeCatalog(options.catalogFile, executionProperties, options.keepGeneratedCatalog);
        if(options.keepGeneratedCatalog) {
            getLog().info("keeping generated catalog: "+catalog.toURI().toURL().toExternalForm());
        }
        return new Resolver(saxonUriResolver, catalog, getLog());
    }
    
    private void createXPathExecutables() throws SaxonApiException {
        setXpExecGetXSpecType(getXPathCompiler().compile("/*/@*"));
        setXpSchGetXSpecFile(getXPathCompiler().compile(
                "iri-to-uri("
                        + "concat("
                        +   "replace(document-uri(/), '(.*)/.*$', '$1'), "
                        +   "'/', "
                        +   "/*[local-name() = 'description']/@schematron))"));
        setXpFileSearcher(getXPathCompiler().compile("//file[@dependency-type!='x:description'][not(starts-with(@abs-uri,'jar:file:'))]"));
    }
    private void createXsltExecutables() throws XSpecPluginException, SaxonApiException, IOException, URISyntaxException {
        getLog().debug("Using XSpec Xslt Compiler: " + xspecResources.getXSpecXslCompilerUri());
        getLog().debug("Using XSpec Xquery Compiler: " + xspecResources.getXSpecXQueryCompilerUri());
        getLog().debug("Using XSpec Reporter: " + xspecResources.getXSpecReporterUri(options.folding));
        getLog().debug("Using Coverage Reporter: " + xspecResources.getXSpecCoverageReporterUri());
        getLog().debug("Using Schematron Dsdl include: " + schematronResources.getSchIsoDsdlIncludeUri());
        getLog().debug("Using Schematron expander: " + schematronResources.getSchIsoAbstractExpandUri());
        getLog().debug("Using Schematrong Svrl: " + schematronResources.getSchIsoSvrlForXslt2Uri());
        getLog().debug("Using Schematron schut: " + xspecResources.getSchematronSchutConverterUri());
        getLog().debug("Using XML dependency scanner: " + pluginResources.getDependencyScannerUri());
        String baseUri = baseDir!=null ? baseDir.toURI().toURL().toExternalForm() : null;

        Source srcXsltCompiler = resolveSrc(xspecResources.getXSpecXslCompilerUri(), baseUri, "XSpec XSL Compiler");
        getLog().debug(xspecResources.getXSpecXslCompilerUri()+" -> "+srcXsltCompiler.getSystemId());
        Source srcXqueryCompiler = resolveSrc(xspecResources.getXSpecXQueryCompilerUri(), baseUri, "XSpec XQuery Compiler");
        getLog().debug(xspecResources.getXSpecXQueryCompilerUri()+" -> "+srcXqueryCompiler.getSystemId());
        Source srcReporter = resolveSrc(xspecResources.getXSpecReporterUri(options.folding), baseUri, "XSpec Reporter");
        getLog().debug(xspecResources.getXSpecReporterUri(options.folding)+" -> "+srcReporter.getSystemId());
        Source srcCoverageReporter = resolveSrc(xspecResources.getXSpecCoverageReporterUri(), baseUri, "Coverage Reporter");
        getLog().debug(xspecResources.getXSpecCoverageReporterUri()+" -> "+srcCoverageReporter.getSystemId());
        Source srcSchIsoDsdl = resolveSrc(schematronResources.getSchIsoDsdlIncludeUri(), baseUri, "Schematron Dsdl");
        getLog().debug(schematronResources.getSchIsoDsdlIncludeUri()+" -> "+srcSchIsoDsdl.getSystemId());
        Source srcSchExpand = resolveSrc(schematronResources.getSchIsoAbstractExpandUri(), baseUri, "Schematron expander");
        getLog().debug(schematronResources.getSchIsoAbstractExpandUri()+" -> "+srcSchExpand.getSystemId());
        Source srcSchSvrl = resolveSrc(schematronResources.getSchIsoSvrlForXslt2Uri(), baseUri, "Schematron Svrl");
        getLog().debug(schematronResources.getSchIsoSvrlForXslt2Uri()+" -> "+srcSchSvrl.getSystemId());
        Source srcSchSchut = resolveSrc(xspecResources.getSchematronSchutConverterUri(), baseUri, "Schematron Schut");
        getLog().debug(xspecResources.getSchematronSchutConverterUri()+" -> "+srcSchSchut.getSystemId());
        Source srcXmlDependencyScanner = resolveSrc(pluginResources.getDependencyScannerUri(), baseUri, "Xml dependency scanner");
        getLog().debug(pluginResources.getDependencyScannerUri()+" -> "+srcXmlDependencyScanner.getSystemId());
        
        // for code coverage
        generateXspecUtilsUri = resolveSrc("generate-tests-utils.xsl", srcXsltCompiler.getSystemId(), "generate-tests-utils.xsl").getSystemId();
        schLocationCompareUri = resolveSrc("../schematron/sch-location-compare.xsl", srcXsltCompiler.getSystemId(), "../schematron/sch-location-compare.xsl").getSystemId();
        setXspec4xsltCompiler(compileXsl(srcXsltCompiler));
        setXspec4xqueryCompiler(compileXsl(srcXqueryCompiler));
        setReporter(compileReporter(srcReporter));
        if(isSaxonPEorEE()) {
            setCoverageReporter(compileXsl(srcCoverageReporter));
        }
        setSchematronDsdl(compileXsl(srcSchIsoDsdl));
        setSchematronExpand(compileXsl(srcSchExpand));
        setSchematronSvrl(compileXsl(srcSchSvrl));
        setSchematronSchut(compileXsl(srcSchSchut));
        setXmlDependencyScanner(compileXsl(srcXmlDependencyScanner));

        setXeSurefire(compileXsl(new StreamSource(getClass().getResourceAsStream("/surefire-reporter.xsl"))));
    }
    public boolean isSaxonPEorEE() {
        String configurationClassName = processor.getUnderlyingConfiguration().getClass().getName();
        return "com.saxonica.config.ProfessionalConfiguration".equals(configurationClassName) ||
                "com.saxonica.config.EnterpriseConfiguration".equals(configurationClassName);
    }
    private XsltExecutable compileReporter(Source reporterSource) throws SaxonApiException, XSpecPluginException, IOException, URISyntaxException {
        if(reporterSource.getSystemId().contains("fold")) {
            getLog().debug("reporter is folding");
            XdmNode xslSource  = documentBuilder.build(reporterSource);
            XsltExecutable xslChanger = getXsltCompiler().compile(resolveSrc(pluginResources.getXsltImageChanger(), null,null));
            XsltTransformer tr = xslChanger.load();
            tr.setParameter(new QName("imgDown"), XdmValue.makeValue(encodeBase64(pluginResources.getImageDown())));
            tr.setParameter(new QName("imgUp"), XdmValue.makeValue(encodeBase64(pluginResources.getImageUp())));
            XdmDestination dest = new XdmDestination();
            tr.setDestination(dest);
            tr.setInitialContextNode(xslSource);
            tr.setBaseOutputURI(reporterSource.getSystemId());
            tr.transform();
            Source transformed = dest.getXdmNode().getUnderlyingNode();
            return getXsltCompiler().compile(transformed);
        } else {
            getLog().debug("reporter is not folding");
            return compileXsl(reporterSource);
        }
    }
    private String encodeBase64(String uri) throws XSpecPluginException, IOException, URISyntaxException {
        Source source = resolveSrc(uri, null, null);
        URL resolvedUri = new URL(source.getSystemId());
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[2048];
        InputStream is = resolvedUri.openStream();
        int read = is.read(buffer);
        long written = 0;
        while(read>0) {
            baos.write(buffer, 0, read);
            read = is.read(buffer);
            written += read;
        }
        baos.flush();
        getLog().debug("image is "+written+" bytes long");
        Base64.Encoder encoder = Base64.getEncoder();
        return encoder.encodeToString(baos.toByteArray());
    }
    private Log getLog() { 
        return log;
    }
    public XsltExecutable compileXsl(Source source) throws SaxonApiException {
        return getXsltCompiler().compile(source);
    }

    public Processor getProcessor() {
        return processor;
    }

    public DocumentBuilder getDocumentBuilder() {
        return documentBuilder;
    }

    public XsltCompiler getXsltCompiler() {
        return xsltCompiler;
    }

    public XPathCompiler getXPathCompiler() {
        return xpathCompiler;
    }

    public XsltExecutable getXspec4xsltCompiler() {
        return xspec4xsltCompiler;
    }

    private void setXspec4xsltCompiler(XsltExecutable xspec4xsltCompiler) {
        this.xspec4xsltCompiler = xspec4xsltCompiler;
    }

    public XsltExecutable getXspec4xqueryCompiler() {
        return xspec4xqueryCompiler;
    }

    private void setXspec4xqueryCompiler(XsltExecutable xspec4xqueryCompiler) {
        this.xspec4xqueryCompiler = xspec4xqueryCompiler;
    }

    public XsltExecutable getReporter() {
        return reporter;
    }

    private void setReporter(XsltExecutable reporter) {
        this.reporter = reporter;
    }
    
    public XsltTransformer getXtReporter() {
        XsltTransformer ret = getReporter().load();
        ret.setParameter(QN_REPORT_CSS, new XdmAtomicValue(RESOURCES_TEST_REPORT_CSS));
        return ret;
    }

    /**
     * The XPath that allows to know which type of XSpec is the file
     * @return The XPath executable
     */
    public XPathExecutable getXpExecGetXSpecType() {
        return xpExecGetXSpecType;
    }

    private void setXpExecGetXSpecType(XPathExecutable xpExecGetXSpecType) {
        this.xpExecGetXSpecType = xpExecGetXSpecType;
    }
    
    public URIResolver getUriResolver() { return xsltCompiler.getURIResolver(); }

    public XsltExecutable getXeSurefire() {
        return xeSurefire;
    }

    private void setXeSurefire(XsltExecutable xeSurefire) {
        this.xeSurefire = xeSurefire;
    }
    
    public Serializer newSerializer(final OutputStream os) { return getProcessor().newSerializer(os); }
    public Serializer newSerializer() { return getProcessor().newSerializer(); }

    public XQueryCompiler getXqueryCompiler() {
        return xqueryCompiler;
    }
    
    private void doAdditionalConfiguration(SaxonOptions saxonOptions) throws XPathException {
        if(saxonOptions!=null) {
            SaxonUtils.configureXsltCompiler(getXsltCompiler(), saxonOptions);
        }
    }

    /**
     * Utility method to resolve a URI, using URIResolver. If resource can not be
     * located, uses <tt>desc</tt> to construct an error message, thrown in
     * <tt>MojoExecutionExcecution</tt>
     * @param source
     * @param baseUri
     * @param desc
     * @return Source
     * @throws XSpecPluginException
     */
    private Source resolveSrc(String source, String baseUri, String desc) throws XSpecPluginException {
        try {
            Source ret = getUriResolver().resolve(source, baseUri);
            if(ret == null) {
                throw new XSpecPluginException("Could not find "+desc+" stylesheet in: "+source);
            }
            return ret;
        } catch(TransformerException ex) {
            throw new XSpecPluginException("while resolving "+source, ex);
        }
    }
    
    private void setSchematronDsdl(XsltExecutable xe) { schDsdl = xe; }
    private void setSchematronExpand(XsltExecutable xe) { schExpand = xe; }
    private void setSchematronSvrl(XsltExecutable xe) { schSvrl = xe; }
    /**
     * Return XSL for <tt>iso_dsdl_include.xsl</tt>
     * @return iso_dsdl_include.xsl XSL
     */
    public XsltExecutable getSchematronDsdl() { return schDsdl; }
    /**
     * Return XSL for <tt>iso_abstract_expand.xsl</tt>
     * @return iso_abstract_expand.xsl XSL
     */
    public XsltExecutable getSchematronExpand() { return schExpand; }
    /**
     * Return XSL for <tt>iso_svrl_for_xslt2.xsl</tt>
     * @return iso_svrl_for_xslt2.xsl XSL
     */
    public XsltExecutable getSchematronSvrl() { return schSvrl; }
    private void setXpSchGetXSpecFile(XPathExecutable xe) { xpSchGetXSpec = xe; }

    private void setSchematronSchut(XsltExecutable xe) { schSchut = xe; }
    /**
     * Return XSL for <tt>schut-to-xspec.xsl</tt>
     * @return schut-to-xspec.xsl XSL
     */
    public XsltExecutable getSchematronSchut() { return schSchut; }
    /**
     * Returns XSL for dependency scanner
     * @return dependency scanner XSL
     */
    public XsltExecutable getXmlDependencyScanner() { return xmlDependencyScanner; }
    private void setXmlDependencyScanner(XsltExecutable xmlDependencyScanner) { this.xmlDependencyScanner = xmlDependencyScanner; }
    /**
     * Return XPath for file searcher
     * @return file seracher XPath
     */
    public XPathExecutable getXpFileSearcher() { return xpFileSearcher; }
    private void setXpFileSearcher(XPathExecutable xpFileSearcher) { this.xpFileSearcher = xpFileSearcher; }
    
    private boolean extendsClass(Class toCheck, Class inheritor) {
        if(toCheck.equals(inheritor)) {
            return true;
        }
        if(toCheck.equals(Object.class)) {
            return false;
        }
        return extendsClass(toCheck.getSuperclass(), inheritor);
    }
    public XsltExecutable getJUnitReporter() { return junitReporter; }
    private void setCoverageReporter(XsltExecutable xe) { coverageReporter = xe; }
    public XsltExecutable getCoverageReporter() { return coverageReporter; }
}
import net.sf.saxon.s9api.*;
import io.xspec.maven.xspecMavenPlugin.XSpecRunner;
import io.xspec.maven.xspecMavenPlugin.resources.SchematronImplResources;
import io.xspec.maven.xspecMavenPlugin.resources.XSpecImplResources;
import io.xspec.maven.xspecMavenPlugin.resources.XSpecPluginResources;
import io.xspec.maven.xspecMavenPlugin.resources.impl.DefaultSchematronImplResources;
import io.xspec.maven.xspecMavenPlugin.resources.impl.DefaultXSpecImplResources;
import io.xspec.maven.xspecMavenPlugin.resources.impl.DefaultXSpecPluginResources;
import io.xspec.maven.xspecMavenPlugin.utils.LogProvider;
import io.xspec.maven.xspecMavenPlugin.utils.RunnerOptions;
import io.xspec.maven.xspecMavenPlugin.utils.XSpecPluginException;
import io.xspec.maven.xspecMavenPlugin.utils.XmlStuff;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import java.io.*;
import java.util.List;
import java.util.Properties;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.project.MavenProject;
import top.marchand.maven.saxon.utils.SaxonOptions;
/**
 * Copyright © 2013, Adam Retter
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the <organization> nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package uk.org.adamretter.maven;


/**
 * xspec-maven-plugin is a plugin that run all xspec unit tests at test phase, and produces reports.
 *
 * It relies on XSpec 1.5.0, available at http://github.com/xspec/xspec
 * 
 * If one unit test fails, the plugin execution fails, and the build fails.
 * 
 * <strong>Saxon implementation</strong>
 * You must define the Saxon implementation (http://www.saxonica.com), as plugin do 
 * not embed any Saxon implementation. Just declare a dependency in plugin :
 * 
 * <pre>
 *   &lt;plugin>
 *     &lt;groupId>io.xspec.maven&lt;/groupId>
 *     &lt;artifactId>xspec-maven-plugin&lt;/artifactId>
 *     &lt;dependencies>
 *       &lt;dependency>
 *         &lt;groupId>net.sf.saxon&lt;/groupId>
 *         &lt;artifactId>Saxon-HE&lt;/artifactId>
 *         &lt;version>9.8.0-5&lt;/version>
 *       &lt;/dependency>
 *     &lt;/dependencies>
 *   &lt<;/plugin>
 * </pre>
 *                 
 * Saxon version must be at least 9.8.0-5. Saxon-PE or Saxon-EE can be used, but you'll 
 * have to deploy them to your local or enterprise repository, as they are not available 
 * in Maven Central. Don't forget to add a dependency to your Saxon license. The license 
 * file may be packaged in a .jar file and deployed to your local or enterprise 
 * repository.
 * 
 * <ul>
 * <li>xspec-maven-plugin expects XSpec files in src/test/xspec/</li>
 * <li>xspec-maven-plugin produces XSpec reports in target/xspec-reports/</li>
 * <li>xspec-maven-plugin produces Surefire reports in target/surefire-reports/</li>
 * </ul>
 * 
 * xspec-maven-plugin respects Maven unit tests convention and supports skipTests 
 * system property. See http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#skipTests
 * 
 * xspec-maven-plugin supports testFailureIgnore configuration parameter. See 
 * http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#testFailureIgnore
 *             
 * <strong>XPath extension functions</strong>
 * 
 * Saxon allows to create XPath extension functions in Java. See https://www.saxonica.com/documentation/index.html#!extensibility/functions.
 * gaulois-pipe has defined a common way to automatically install extension functions 
 * in Saxon. xspec-maven-plugin supports the same mecanism. It looks in classpath for 
 * <tt>META-INF/services/top.marchand.xml.gaulois.xml</tt> resources.
 * Each file declares extension functions in this format :
 * 
 * <pre>       
 *   &lt;gaulois-services>
 *     &lt;saxon>
 *       &lt;extensions>
 *         &lt;function>top.marchand.xml.extfunctions.basex.BaseXQuery&lt;/function>
 *       &lt;/extensions>
 *     &lt;/saxon>
 *   &lt;/gaulois-services>
 * </pre>
 *             
 * At least two function libraries are available and tested with xspec-maven-plugin : 
 * <ul>
 * <li>https://github.com/cmarchand/xpath-basex-ext/</li>
 * <li>https://github.com/AxelCourt/saxon-marklogic-ext</li>
 * </ul>
 * 
 * If you want want to add your own extension functions to XSpec engine, create a maven 
 * project with function implementation, a service file, and add it as a dependency to 
 * xspec-maven-plugin delaration :
 * 
 * <pre>
 *   &lt;plugin>
 *     &lt;groupId>io.xspec.maven&lt;/groupId>
 *     &lt;artifactId>xspec-maven-plugin&lt;/artifactId>
 *     &lt;dependencies>
 *       &lt;dependency>
 *         &lt;groupId>your.enterprise.groupId&lt;/groupId>
 *         &lt;artifactId>XPath-extension-functions&lt;/artifactId>
 *       &lt;/dependency>
 *     &lt;/dependencies>
 *   &lt;/plugin>
 * </pre>
 *             
 * All extension functions found will create a log in console when installed in Saxon.
 *
 * @author <a href="mailto:adam.retter@googlemail.com">Adam Retter</a>
 * @author <a href="mailto:christophe@marchand.top">Christophe Marchand</a>
 */
@Mojo(name = "run-xspec", defaultPhase = LifecyclePhase.TEST, requiresDependencyResolution = ResolutionScope.TEST)
public class XSpecMojo extends AbstractMojo implements LogProvider {
    public static final transient String XSPEC_PREFIX = "dependency://io.xspec+xspec/";
    public static final transient String XML_UTILITIES_PREFIX = "dependency://org.mricaud+xml-utilities/";
    public static final transient String CATALOG_NS = "urn:oasis:names:tc:entity:xmlns:xml:catalog";
    public static final transient String XSPEC_NS = "http://www.jenitennison.com/xslt/xspec";
    public static final transient String LOCAL_PREFIX = "dependency://io.xspec.maven+xspec-maven-plugin/";

    @Parameter( defaultValue = "${session}", readonly = true )
    private MavenSession session;
    
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    public MavenProject project;

    /**
     * Defines if XSpec unit tests should be run or skipped.
     * It's a bad practise to set this option, and this NEVER be done.
     */
    @Parameter(property = "skipTests", defaultValue = "false")
    public boolean skipTests;

    /**
     * Path to compiler/generate-xspec-tests.xsl XSpec implementation file.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String xspecXslCompiler;

    /**
     * Path to compiler/generate-query-tests.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String xspecXQueryCompiler;
    
    /**
     * Path to schematron/iso-schematron/iso_dsdl_include.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String schIsoDsdlInclude;
    
    /**
     * Path to schematron/iso-schematron/iso_abstract_expand.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String schIsoAbstractExpand;
    
    /**
     * Path to schematron/iso-schematron/iso_svrl_for_xslt2.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String schIsoSvrlForXslt2;

    /**
     * Path to schematron/schut-to-xspec.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String schSchut;
    /**
     * Path to reporter/format-xspec-report.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String xspecReporter;
        
    /**
     * Path to reporter/coverage-report.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String coverageReporter;

    /**
     * Path to org/mricaud/xml-utilities/get-xml-file-static-dependency-tree.xsl.
     * This parameter is only available for developement purposes, and should never be overriden.
     */
    @Parameter()
    public String dependencyScanner;

    /**
     * Directory where XSpec files are search
     */
    @Parameter(defaultValue = "${project.basedir}/src/test/xspec", required = true)
    public File testDir;
    
    /**
     * The global Saxon options. 
     * See https://github.com/cmarchand/saxonOptions-mvn-plug-utils/wiki for full documentation.
     * It allows to configure Saxon as it'll be used by plugin to run XSpecs. 
     * The main option that might be configured is xi, to activate or not XInclude.
     * <pre>
     * &lt;configuration>
     *   &lt;saxonOptions>
     *     &lt;xi>on&lt;/xi>
     *   &lt;/saxonOptions>
     * &lt;/configuration>
     * </pre>
     */
    @Parameter(name = "saxonOptions")
    public SaxonOptions saxonOptions;

    /**
     * Patterns fo files to exclude
     * Each found file that ends with an excluded value will be skipped.
     * <pre>
     *  &lt;configuration>
     *    &lt;excludes>
     *      &lt;exclude>-TI.xspec&lt;/exclude>
     *    &lt;/excludes>
     *  &lt;/configuration>
     * </pre>
     * Each file that ends with -TI.xspec will be skipped.
     */
    @Parameter(alias = "excludes")
    public List<String> excludes;
    
    /**
     * Defines if a test failure should fail the build, or not.
     * This option should NEVER be used.
     */
    @Parameter(defaultValue = "${maven.test.failure.ignore}")
    public boolean testFailureIgnore;

    /**
     * The directory where report files will be created
     */
    @Parameter(defaultValue = "${project.build.directory}/xspec-reports", required = true)
    public File reportDir;
    
    /**
     * The directory where JUnit final report will be created.
     * xspec-maven-plugin produces on junit report file per XSpec file, in 
     * <tt>reportDir</tt> directory, and creates a merged report, in <tt>junitReportDir</tt>, 
     * named <tt>TEST-xspec&lt;suffix>.xml</tt>.
     * suffix depends on execution id
     */
    @Parameter(defaultValue = "${project.build.directory}/surefire-reports", required = true)
    public File junitReportDir;

    /**
     * The catalog file to use.
     * It must conform to OASIS catalog specification. 
     * See https://www.oasis-open.org/committees/entity/spec-2001-08-06.html. 
     * If defined, this catalog must be provided, or generated before xspec-maven-plugin execution.
     * It can be an absolute or relative path. All relative pathes are relative to ${project.basedir}.
     */
    @Parameter(defaultValue = "${catalog.filename}")
    public String catalogFile;

    /**
     * The directory where surefire report will be created
     */
    @Parameter(defaultValue = "${project.build.directory}/surefire-reports", required = true)
    public File surefireReportDir;

    /**
     * Defines if generated catalog should be kept or not.
     * xspec-maven-plugin generates its own catalog to access its own resources, 
     * and if <tt>catalogFile</tt> is defined, adds a <tt>&lt;next-catalog /></tt>
     * entry in this generated catalog.
     * Only usefull to debug plugin.
     */
    @Parameter(defaultValue = "false")
    public Boolean keepGeneratedCatalog;
    
    /**
     * Define if coverage report must be used. Coverage is applied only on XSLT unit tests,
     * if and only if Saxon PE or EE is used.
     */
    @Parameter(defaultValue = "false")
    private boolean coverage;
    
    /**
     * Define if report must use folding presentation.
     */
    @Parameter(defaultValue = "false")
    private boolean folding;
    
    @Parameter(defaultValue = "${mojoExecution}", readonly = true)
    public MojoExecution execution;
    

    // package private for tests
    XmlStuff xmlStuff;
    
    // package private for test purpose
    boolean uriResolverSet = false;
    public static final QName QN_NAME = new QName("name");
    public static final QName QN_SELECT = new QName("select");
    
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (isSkipTests()) {
            getLog().info("'skipTests' is set... skipping XSpec tests!");
            return;
        }
        XSpecImplResources xspecResources = getXSpecResources();
        SchematronImplResources schResources = getSchematronResources();
        XSpecPluginResources pluginResources = getXSpecPluginResources();
        RunnerOptions options = new RunnerOptions(
                project.getBasedir(), 
                keepGeneratedCatalog, 
                catalogFile, 
                excludes, 
                testDir,
                reportDir,
                execution==null ? null : execution.getExecutionId(),
                surefireReportDir,
                coverage,
                folding);
        Properties environment = new Properties();
        environment.putAll(session.getUserProperties());
        environment.putAll(session.getSystemProperties());
        XSpecRunner runner = new XSpecRunner(getLog(), project.getBasedir());
        runner.setResources(xspecResources, schResources, pluginResources);
        runner.setEnvironment(environment, options);

        try {
            runner.init(saxonOptions);
            runner.execute();
        } catch(XSpecPluginException ex) {
            if(!testFailureIgnore) {
                throw new MojoFailureException("Some XSpec tests failed or were missed!");
            } else {
                getLog().warn("Some XSpec tests failed or were missed, but build will not fail!");
            }
        } finally {
            try {
                runner.generateIndex();
            } catch(XSpecPluginException ex2) { }
        }
    }
    

    protected boolean isSkipTests() {
        return skipTests;
    }
    
    static final String XSPEC_MOJO_PFX = "[xspec-mojo] ";
    
    private XSpecImplResources getXSpecResources() {
        DefaultXSpecImplResources ret = new DefaultXSpecImplResources();