org.daisy.maven.xspec.XSpecRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.daisy.maven.xspec.XSpecRunner.java

Source

/**
 * Copyright (C) 2013 The DAISY Consortium
 *
 * Licensed 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.daisy.maven.xspec;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.ServiceLoader;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamSource;

import net.sf.saxon.s9api.MessageListener;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.Serializer;
import net.sf.saxon.s9api.XPathCompiler;
import net.sf.saxon.s9api.XdmAtomicValue;
import net.sf.saxon.s9api.XdmDestination;
import net.sf.saxon.s9api.XdmItem;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.s9api.XdmValue;
import net.sf.saxon.s9api.XsltCompiler;
import net.sf.saxon.s9api.XsltExecutable;
import net.sf.saxon.s9api.XsltTransformer;
import net.sf.saxon.trans.XPathException;

import org.apache.xml.resolver.CatalogManager;
import org.apache.xml.resolver.tools.CatalogResolver;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.common.io.Resources;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;

@Component(name = "xspec-runner", service = { XSpecRunner.class })
public class XSpecRunner {

    private final static String XSPEC_NAMESPACE = "http://www.jenitennison.com/xslt/xspec";
    private final static QName XSPEC_MAIN_TEMPLATE = new QName("x", XSPEC_NAMESPACE, "main");
    private static final String XSPEC_CSS_NAME = "xspec-report.css";
    private static final QName XSPEC_CSS_URI_PARAM = new QName("report-css-uri");
    private static final XdmValue XSPEC_CSS_URI = new XdmAtomicValue(XSPEC_CSS_NAME);
    private static final QName JUNIT_NAME_PARAM = new QName("name");;
    private static final QName JUNIT_TIME_PARAM = new QName("time");;

    private Processor processor;
    private URIResolver defaultResolver;
    private XPathCompiler xpathCompiler;
    private XsltExecutable xspecCompilerLoader;
    private XsltExecutable xspecHtmlFormatterLoader;
    private XsltExecutable xspecHtmlSummaryFormatterLoader;
    private XsltExecutable xspecJUnitFormatterLoader;
    private ByteSource cssSupplier;

    @Reference(name = "Processor", unbind = "-", service = Processor.class, cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC)
    public void setProcessor(Processor processor) {
        this.processor = processor;
    }

    public boolean hasFocus(File testDir) {
        return hasFocus(listXSpecFilesRecursively(testDir));
    }

    public boolean hasFocus(Collection<File> tests) {
        for (File test : tests) {
            if (test.exists()) {
                try {
                    XdmNode node = new XdmNode(
                            processor.getUnderlyingConfiguration().buildDocument(new StreamSource(test)));
                    if (((XdmAtomicValue) xpathCompiler.evaluateSingle("exists(//scenario[@focus])", node))
                            .getBooleanValue())
                        return true;
                } catch (SaxonApiException e) {
                } catch (XPathException e) {
                }
            }
        }
        return false;
    }

    public TestResults run(Map<String, File> tests, File reportDir) {
        Set<String> focusTests = new HashSet<String>();
        {
            for (String testName : tests.keySet()) {
                File test = tests.get(testName);
                if (test.exists()) {
                    try {
                        XdmNode node = new XdmNode(
                                processor.getUnderlyingConfiguration().buildDocument(new StreamSource(test)));
                        if (((XdmAtomicValue) xpathCompiler.evaluateSingle("exists(//scenario[@focus])", node))
                                .getBooleanValue())
                            focusTests.add(testName);
                    } catch (SaxonApiException e) {
                    } catch (XPathException e) {
                    }
                }
            }
        }
        Set<String> skipTests = new HashSet<String>();
        {
            if (!focusTests.isEmpty())
                for (String testName : tests.keySet())
                    if (!focusTests.contains(testName))
                        skipTests.add(testName);
        }
        TestResults.Builder builder = new TestResults.Builder("");
        for (Map.Entry<String, File> test : tests.entrySet()) {
            if (skipTests.contains(test.getKey()))
                continue;
            builder.addSubResults(runSingle(test.getKey(), test.getValue(), reportDir));
        }
        writeSummaryReport(focusTests.isEmpty() ? tests.keySet() : focusTests, reportDir);
        return builder.build();
    }

    public TestResults run(File testDir, File reportDir) {
        Map<String, File> tests = new HashMap<String, File>();
        for (File file : listXSpecFilesRecursively(testDir))
            tests.put(file.getAbsolutePath().substring(testDir.getAbsolutePath().length() + 1)
                    .replaceAll("\\.xspec$", "").replaceAll("[\\./\\\\]", "_"), file);
        return run(tests, reportDir);
    }

    private TestResults runSingle(String testName, File testFile, File reportDir) {
        // Prepare the reporters
        File textReport = new File(reportDir, "OUT-" + testName + ".txt");
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(Files.newWriter(textReport, Charsets.UTF_8));
        } catch (FileNotFoundException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        SaxonReporter saxonReporter = new SaxonReporter(writer);

        XdmDestination xspecTestResult = new XdmDestination();
        XdmDestination xspecTestCompiled = new XdmDestination();
        SaxonApiException executionException = null;

        Stopwatch stopwatch = Stopwatch.createStarted();
        report("Running " + testName, writer);

        try {
            // Compile the XSPec test into an executable XSLT
            XsltTransformer xspecCompiler = xspecCompilerLoader.load();
            Source testAsSource = new StreamSource(testFile);
            xspecCompiler.setSource(testAsSource);
            xspecCompiler.setDestination(xspecTestCompiled);
            xspecCompiler.setErrorListener(saxonReporter);
            xspecCompiler.setMessageListener(saxonReporter);
            xspecCompiler.setURIResolver(new XSpecResolver(xspecCompiler.getURIResolver()));
            xspecCompiler.transform();

            // Create a new URI resolver if a mock catalog is present
            File catalog = new File(testFile.getParentFile(), "catalog.xml");
            URIResolver testResolver = defaultResolver;
            if (catalog.exists()) {
                CatalogManager catman = new CatalogManager();
                catman.setUseStaticCatalog(false);
                catman.setCatalogFiles(catalog.getPath());
                testResolver = new CatalogResolver(catman);
            }

            // Run the compiled XSpec test
            XsltCompiler xspecTestCompiler = processor.newXsltCompiler();
            xspecTestCompiler.setURIResolver(new XSpecResolver(testResolver));
            processor.getUnderlyingConfiguration().setErrorListener(saxonReporter);
            Source compiledTestAsSource = xspecTestCompiled.getXdmNode().asSource();
            XsltTransformer xspecTestRunner = xspecTestCompiler.compile(compiledTestAsSource).load();
            xspecTestRunner.setInitialTemplate(XSPEC_MAIN_TEMPLATE);
            xspecTestRunner.setDestination(xspecTestResult);
            xspecTestRunner.setErrorListener(saxonReporter);
            xspecTestRunner.setMessageListener(saxonReporter);
            xspecTestRunner.setURIResolver(new XSpecResolver(testResolver));
            xspecTestRunner.transform();

        } catch (SaxonApiException e) {
            report(e.getMessage(), writer);
            e.printStackTrace(writer);
            executionException = e;
        }

        stopwatch.stop();

        TestResults result = (executionException == null)
                ? XSpecResultBuilder.fromReport(testName, xspecTestResult.getXdmNode(), xpathCompiler,
                        stopwatch.toString())
                : XSpecResultBuilder.fromException(testName, executionException, stopwatch.toString());

        report(result.toString(), writer);

        writer.close();

        if (result.getErrors() == 0) {
            try {
                // Write XSpec report
                File xspecReport = new File(reportDir, "XSPEC-" + testName + ".xml");
                serializeToFile(xspecReport).serializeNode(xspecTestResult.getXdmNode());

                // Write HTML report
                File css = new File(reportDir, XSPEC_CSS_NAME);
                if (!css.exists()) {
                    cssSupplier.copyTo(new FileOutputStream(css));
                }
                File htmlReport = new File(reportDir, "HTML-" + testName + ".html");
                XsltTransformer htmlFormatter = xspecHtmlFormatterLoader.load();
                htmlFormatter.setSource(xspecTestResult.getXdmNode().asSource());
                htmlFormatter.setParameter(XSPEC_CSS_URI_PARAM, XSPEC_CSS_URI);
                htmlFormatter.setDestination(serializeToFile(htmlReport));
                htmlFormatter.setMessageListener(SaxonSinkReporter.INSTANCE);
                htmlFormatter.transform();

                // Write Surefire report
                File surefireReport = new File(reportDir, "TEST-" + testName + ".xml");
                XsltTransformer junitFormatter = xspecJUnitFormatterLoader.load();
                junitFormatter.setSource(xspecTestResult.getXdmNode().asSource());
                junitFormatter.setDestination(serializeToFile(surefireReport));
                junitFormatter.setParameter(JUNIT_NAME_PARAM, new XdmAtomicValue(testName));
                junitFormatter.setParameter(JUNIT_TIME_PARAM,
                        new XdmAtomicValue(stopwatch.elapsed(TimeUnit.MILLISECONDS) / 1000d));
                junitFormatter.transform();
            } catch (SaxonApiException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        return result;
    }

    /*
     * Write HTML summary report
     * Assumes XSpec reports were written to <reportDir>/XSPEC-<testName>.xml
     * and HTML reports were written to <reportDir>/HTML-<testName>.html
     */
    private void writeSummaryReport(Set<String> testNames, File reportDir) {
        try {
            XsltTransformer formatter = xspecHtmlSummaryFormatterLoader.load();
            formatter.setInitialTemplate(new QName("main"));
            formatter.setParameter(new QName("test-names"), new XdmValue(
                    Collections2.<String, XdmItem>transform(testNames, new Function<String, XdmItem>() {
                        public XdmItem apply(String s) {
                            return new XdmAtomicValue(s);
                        }
                    })));
            formatter.setParameter(new QName("report-dir"), new XdmAtomicValue(reportDir.toURI()));
            formatter.setDestination(serializeToFile(new File(reportDir, "index.html")));
            formatter.setMessageListener(SaxonSinkReporter.INSTANCE);
            formatter.transform();
        } catch (SaxonApiException e) {
            throw new RuntimeException(e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    private Serializer serializeToFile(File file) throws FileNotFoundException {
        Serializer serializer = processor.newSerializer();
        serializer.setOutputStream(new FileOutputStream(file));
        serializer.setCloseOnCompletion(true);
        return serializer;
    }

    @Activate
    public void init() {
        try {

            System.setProperty("xml.catalog.ignoreMissing", "true");
            if (processor == null)
                processor = new Processor(false);
            defaultResolver = processor.getUnderlyingConfiguration().getURIResolver();

            XsltCompiler xsltCompiler = processor.newXsltCompiler();
            xsltCompiler.setURIResolver(new XSpecResolver(xsltCompiler.getURIResolver()));
            // Initialize the XSpec compiler
            xspecCompilerLoader = xsltCompiler.compile(getXSpecSource("/xspec/compiler/generate-xspec-tests.xsl"));

            // Initialize the XSpec report formatter
            xspecHtmlFormatterLoader = xsltCompiler
                    .compile(getXSpecSource("/xspec/reporter/format-xspec-report.xsl"));

            // Initialize the XSpec summary formatter
            xspecHtmlSummaryFormatterLoader = xsltCompiler
                    .compile(getXSpecSource("/xspec-extra/format-xspec-summary.xsl"));

            // Initialize the JUnit report formatter
            xspecJUnitFormatterLoader = xsltCompiler
                    .compile(getXSpecSource("/xspec-extra/format-junit-report.xsl"));

            // Configure the XPath compiler used to parse the XSpec report
            xpathCompiler = processor.newXPathCompiler();
            xpathCompiler.setCaching(true);
            xpathCompiler.declareNamespace("", XSPEC_NAMESPACE);

            // Input supplier for the report CSS
            cssSupplier = Resources.asByteSource(XSpecRunner.class.getResource("/xspec/reporter/test-report.css"));

        } catch (SaxonApiException e) {
            throw new IllegalStateException(e);
        }
    }

    private static void report(String message, PrintWriter writer) {
        System.out.println(message);
        writer.println(message);
    }

    private static Source getXSpecSource(String path) {
        return new StreamSource(XSpecRunner.class.getResourceAsStream(path), "xspec:" + path);
    }

    /*
     * FileUtils.listFiles from Apache Commons IO could be used here as well,
     * but would introduce another dependency.
     */
    public static Set<File> listXSpecFilesRecursively(File directory) {
        ImmutableSet.Builder<File> builder = new ImmutableSet.Builder<File>();
        if (directory.isDirectory())
            for (File file : directory.listFiles()) {
                if (file.isDirectory())
                    builder.addAll(listXSpecFilesRecursively(file));
                else if (file.getName().endsWith(".xspec"))
                    builder.add(file);
            }
        return builder.build();
    }

    private static class SaxonReporter implements ErrorListener, MessageListener {

        private final PrintWriter writer;

        public SaxonReporter(PrintWriter writer) {
            this.writer = writer;
        }

        public void warning(TransformerException exception) throws TransformerException {
            writer.println(exception.getMessage());
        }

        public void error(TransformerException exception) throws TransformerException {
            writer.println(exception.getMessage());
        }

        public void fatalError(TransformerException exception) throws TransformerException {
            writer.println(exception.getMessage());
        }

        public void message(XdmNode content, boolean terminate, SourceLocator locator) {
            writer.println(content);
        }

    }

    private static class SaxonSinkReporter implements ErrorListener, MessageListener {

        static SaxonSinkReporter INSTANCE = new SaxonSinkReporter();

        private SaxonSinkReporter() {
        }

        public void warning(TransformerException exception) throws TransformerException {
        }

        public void error(TransformerException exception) throws TransformerException {
        }

        public void fatalError(TransformerException exception) throws TransformerException {
        }

        public void message(XdmNode content, boolean terminate, SourceLocator locator) {
        }

    }

    private static class XSpecResolver implements URIResolver {

        private final URIResolver delegate;

        public XSpecResolver(URIResolver delegate) {
            this.delegate = delegate;
        }

        @Override
        public Source resolve(String href, String base) throws TransformerException {
            try {
                String uri = new URI(base).resolve(href).toString();
                if (uri.startsWith("xspec:")) {
                    InputStream is = XSpecRunner.class.getResourceAsStream(uri.substring(6));
                    return new StreamSource(is, uri);
                } else if (Pattern.compile("\\.(zip|jar)!/").matcher(uri).find() && uri.startsWith("file:")) {
                    Source s = delegate.resolve("jar:" + uri, base);
                    if (s != null)
                        return s;
                }
            } catch (URISyntaxException e) {
                // Do nothing
            }
            return delegate.resolve(href, base);
        }

    }
}