All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.xmlcalabash.model.Parser Maven / Gradle / Ivy

The newest version!
package com.xmlcalabash.model;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.slf4j.Logger;

import javax.xml.XMLConstants;
import javax.xml.transform.sax.SAXSource;

import com.xmlcalabash.core.XProcConstants;
import com.xmlcalabash.core.XProcException;
import com.xmlcalabash.core.XProcRuntime;
import com.xmlcalabash.extensions.UntilUnchanged;
import com.xmlcalabash.util.*;
import net.sf.saxon.PreparedStylesheet;
import net.sf.saxon.functions.FunctionLibrary;
import net.sf.saxon.query.QueryModule;
import net.sf.saxon.query.StaticQueryContext;
import net.sf.saxon.query.XQueryExpression;
import net.sf.saxon.query.XQueryFunctionLibrary;
import net.sf.saxon.s9api.Axis;
import net.sf.saxon.s9api.DocumentBuilder;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.XQueryCompiler;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.s9api.XdmNodeKind;
import net.sf.saxon.s9api.XdmSequenceIterator;
import net.sf.saxon.s9api.XsltCompiler;
import net.sf.saxon.s9api.XsltExecutable;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 *
 * @author ndw
 */
public class Parser {
    // TODO: Make new QName() values throughout static
    private static QName px_name = new QName(XProcConstants.NS_CALABASH_EX,"name");
    private static QName _name = new QName("name");
    private static QName _href = new QName("href");
    private static QName _type = new QName("type");
    private static QName _version = new QName("version");
    private static QName _namespace = new QName("namespace");
    private static QName err_XS0063 = new QName(XProcConstants.NS_XPROC_ERROR, "XS0063");
    private static QName p_use_when = new QName(XProcConstants.NS_XPROC, "use-when");
    private static QName _use_when = new QName("use-when");
    private static QName _exclude_inline_prefixes = new QName("exclude-inline-prefixes");
    private static QName cx_import = new QName(XProcConstants.NS_CALABASH_EX, "import");

    private XProcRuntime runtime = null;
    private boolean loadingStandardLibrary = false;
    private Logger logger = LoggerFactory.getLogger(Parser.class);
    private static int importCount = 0;

    public Parser(XProcRuntime runtime) {
        this.runtime = runtime;
    }

    @Deprecated
    public DeclareStep loadPipeline(InputStream inputStream) throws SaxonApiException, IOException {
        return loadPipeline(inputStream, null);
    }

    public DeclareStep loadPipeline(InputStream inputStream, String base) throws SaxonApiException, IOException {
        InputSource is = new InputSource(inputStream);
        if (base != null) {
            is.setSystemId(base);
        }
        try {
            XdmNode doc = runtime.parse(is);
            return loadPipeline(doc);
        } finally {
            inputStream.close();
        }
    }

    public DeclareStep loadPipeline(String uri) throws SaxonApiException {
        return loadPipeline(uri, URIUtils.cwdAsURI().toASCIIString());
    }

    public DeclareStep loadPipeline(String uri, String base) throws SaxonApiException {
        XdmNode doc = runtime.parse(uri, base);
        return loadPipeline(doc);
    }

    private DeclareStep loadPipeline(XdmNode doc) throws SaxonApiException {
        XdmNode root = S9apiUtils.getDocumentElement(doc);

        if (!XProcConstants.p_declare_step.equals(root.getNodeName())
            && !XProcConstants.p_pipeline.equals(root.getNodeName())) {
            throw new UnsupportedOperationException("Pipelines must be p:pipeline or p:declare-step documents");
        }

        return usePipeline(root);
    }

    public DeclareStep usePipeline(XdmNode node) {
        if (node.getNodeKind() == XdmNodeKind.DOCUMENT) {
            node = S9apiUtils.getDocumentElement(node);
        }
        DeclareStep decl = readDeclareStep(node, runtime);
        parseDeclareStepBodyPassTwo(decl);
        return decl;
    }


    public PipelineLibrary loadStandardLibrary() throws FileNotFoundException, URISyntaxException, SaxonApiException {
        URI home = URIUtils.homeAsURI();
        URI cwd = URIUtils.cwdAsURI();
        URI puri = home;

        InputStream instream = getClass().getResourceAsStream("/etc/pipeline-library.xml");
        if (instream == null) {
            throw new UnsupportedOperationException("Failed to load standard pipeline library from JAR file");
        }

        XdmNode doc = parse(instream, puri);
        XdmNode root = S9apiUtils.getDocumentElement(doc);

        if (!XProcConstants.p_library.equals(root.getNodeName())) {
            throw new UnsupportedOperationException("Pipeline libraries must be p:library documents");
        }

        loadingStandardLibrary = true;
        // only called once, no need to cache
        // FIXME: not entirely true: it is called whenever reset() is called which can happen multiple times
        // => instead run it lazily?
        PipelineLibrary library = readLibrary(root, null);
        loadingStandardLibrary = false;

        return library;
    }

    private XdmNode loadExtensionLibrary() throws FileNotFoundException, URISyntaxException, SaxonApiException {
        URI home = URIUtils.homeAsURI();
        URI puri = home;

        InputStream instream = getClass().getResourceAsStream("/etc/extension-library.xml");
        if (instream == null) {
            throw new UnsupportedOperationException("Failed to load XProc pipeline library from JAR file");
        }

        XdmNode doc = parse(instream, puri);
        XdmNode root = S9apiUtils.getDocumentElement(doc);

        if (!XProcConstants.p_library.equals(root.getNodeName())) {
            throw new UnsupportedOperationException("Pipeline libraries must be p:library documents");
        }

        return doc;
    }

    @Deprecated
    public PipelineLibrary loadLibrary(InputStream libraryInputStream) throws SaxonApiException, IOException {
        return loadLibrary(libraryInputStream, null);
    }

    public PipelineLibrary loadLibrary(InputStream libraryInputStream, String base) throws SaxonApiException, IOException {
        InputSource is = new InputSource(libraryInputStream);
        if (base != null) {
            is.setSystemId(base);
        }

        try {
            XdmNode doc = runtime.parse(is);
            XdmNode root = S9apiUtils.getDocumentElement(doc);
            return useLibrary(root);
        } finally {
            libraryInputStream.close();
        }
    }

    public PipelineLibrary loadLibrary(String libraryURI) throws SaxonApiException {
        XdmNode doc = runtime.parse(libraryURI, URIUtils.cwdAsURI().toASCIIString());
        XdmNode root = S9apiUtils.getDocumentElement(doc);
        return useLibrary(root);
    }

    public PipelineLibrary useLibrary(XdmNode root) throws SaxonApiException {
        if (!XProcConstants.p_library.equals(root.getNodeName())) {
            throw new UnsupportedOperationException("Pipelines libraries must be p:library documents");
        }
        return readLibrary(root, null);
    }

    private XdmNode parse(InputStream instream, URI baseURI) throws SaxonApiException {
        try {
            XMLReader reader = XMLReaderFactory.createXMLReader();
            reader.setEntityResolver(runtime.getResolver());
            SAXSource source = new SAXSource(reader, new InputSource(instream));
            DocumentBuilder builder = runtime.getProcessor().newDocumentBuilder();
            builder.setLineNumbering(true);
            builder.setDTDValidation(false);
            builder.setBaseURI(baseURI);
            return builder.build(source);
        } catch (SAXException se) {
            throw new XProcException(se);
        }
    }

    private Map parsedLibraries = new HashMap<>();

    private PipelineLibrary readLibrary(XdmNode node, URI baseURI) {
        if (!XProcConstants.p_library.equals(node.getNodeName())
                && !XProcConstants.p_pipeline.equals(node.getNodeName())
                && !XProcConstants.p_declare_step.equals(node.getNodeName())) {
            runtime.error(null, XProcException.staticError(52, node, "Not a pipeline or library: " + node.getNodeName()));
            return null;
        }

        if (baseURI != null && parsedLibraries.containsKey(baseURI))
            return parsedLibraries.get(baseURI);

        PipelineLibrary library = new PipelineLibrary(runtime, node);
        if (!loadingStandardLibrary)
            library.addImport(runtime.getStandardLibrary());

        if (baseURI != null)
            parsedLibraries.put(baseURI, library);

        if (XProcConstants.p_library.equals(node.getNodeName())) {
            checkAttributes(node, new String[] { "xpath-version", "psvi-required", "version", "exclude-inline-prefixes"}, false);

            library.setVersion(inheritedVersion(node));

            // Read all the steps and make them available
            for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
                if (snode.getNodeName().equals(XProcConstants.p_import)) {
                    // skip it
                } else {
                    Step substep = readStep(library, library, snode);

                    if (substep instanceof DeclareStep) {
                        ((DeclareStep)substep).setSourceImport(baseURI);
                        library.addStep((DeclareStep) substep);
                    } else {
                        throw new UnsupportedOperationException("A p:library must contain only p:pipeline and p:declare-steps.");
                    }
                }
            }

            for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
                if (snode.getNodeName().equals(XProcConstants.p_import)) {
                    Step substep = readStep(library, library, snode);
                    Import importElem = (Import) substep;
                    XdmNode root = importElem.getRoot();
                    PipelineLibrary lib = readLibrary(root, importElem.getHref());
                    if (lib != null)
                        library.addImport(lib);
                }
            }
        } else {
            Step substep = readStep(library, library, node);

            if (XProcConstants.NS_CALABASH_EX.equals(substep.getDeclaredType().getNamespaceURI())
                    && (substep.getDeclaredType().getLocalName().startsWith("anonymousType"))) {
                throw XProcException.staticError(53, node, "No type attribute on imported pipeline.");
            }

            if (substep instanceof DeclareStep) {
                ((DeclareStep)substep).setSourceImport(baseURI);
                library.addStep((DeclareStep) substep);
            } else {
                throw new UnsupportedOperationException("A p:library must contain only p:pipeline and p:declare-steps.");
            }
        }

        checkExtensionAttributes(node, library);

        return library;
    }

    private Vector readSignature(Step step) {
        Vector rest = new Vector ();
        boolean allowPrimary = false;
        boolean allowVariables = false;
        int primaryParamInputCount = 0;
        int primaryDocInputCount = 0;
        int primaryOutputCount = 0;

        HashSet sig = new HashSet ();
        if (XProcConstants.p_pipeline.equals(step.getType())
                || XProcConstants.p_declare_step.equals(step.getType())) {
            sig.add(XProcConstants.p_import);
            sig.add(XProcConstants.p_input);
            sig.add(XProcConstants.p_output);
            sig.add(XProcConstants.p_log);
            sig.add(XProcConstants.p_option);
            sig.add(XProcConstants.p_serialization);
            allowVariables = true;
            allowPrimary = XProcConstants.p_declare_step.equals(step.getType()); // Not on p:pipeline
        } else if (XProcConstants.p_for_each.equals(step.getType())) {
            sig.add(XProcConstants.p_iteration_source);
            sig.add(XProcConstants.p_output);
            sig.add(XProcConstants.p_log);
            allowVariables = true;
            allowPrimary = true;
        } else if (XProcConstants.cx_until_unchanged.equals(step.getType())) {
            sig.add(XProcConstants.p_iteration_source);
            sig.add(XProcConstants.p_output);
            sig.add(XProcConstants.p_log);
            allowVariables = true;
            allowPrimary = true;
        } else if (XProcConstants.p_viewport.equals(step.getType())) {
            sig.add(XProcConstants.p_viewport_source);
            sig.add(XProcConstants.p_output);
            sig.add(XProcConstants.p_log);
            allowVariables = true;
            allowPrimary = true;
        } else if (XProcConstants.p_choose.equals(step.getType())) {
            sig.add(XProcConstants.p_xpath_context);
            allowVariables = true;
            allowPrimary = true;
        } else if (XProcConstants.p_when.equals(step.getType())) {
            sig.add(XProcConstants.p_xpath_context);
            sig.add(XProcConstants.p_output);
            sig.add(XProcConstants.p_log);
            allowVariables = true;
            allowPrimary = true;
        } else if (XProcConstants.p_group.equals(step.getType())
                   || XProcConstants.p_catch.equals(step.getType())
                   || XProcConstants.p_otherwise.equals(step.getType())) {
            sig.add(XProcConstants.p_output);
            sig.add(XProcConstants.p_log);
            allowVariables = true;
            allowPrimary = true;
        } else if (XProcConstants.p_try.equals(step.getType())) {
            allowVariables = true;
        } else {
            sig.add(XProcConstants.p_input);
            sig.add(XProcConstants.p_log);
            sig.add(XProcConstants.p_with_option);
            sig.add(XProcConstants.p_with_param);
        }

        Hashtable serializations = new Hashtable ();
        Hashtable loggers = new Hashtable ();

        int pos = 1;
        boolean done = false;
        for (XdmNode node : new AxisNodes(runtime, step.getNode(), Axis.CHILD, AxisNodes.PIPELINE)) {
            if (done) {
                if (node.getNodeKind() == XdmNodeKind.TEXT) {
                    throw XProcException.staticError(37, node, "Unexpected text: " + node.getStringValue());
                }

                rest.add(node);
                continue;
            }

            if (sig.contains(node.getNodeName())) {
                QName nodeName = node.getNodeName();

                if (XProcConstants.p_input.equals(nodeName)
                    || XProcConstants.p_iteration_source.equals(nodeName)
                    || XProcConstants.p_viewport_source.equals(nodeName)
                    || XProcConstants.p_xpath_context.equals(nodeName)) {
                    Input input = readInput(step, node);

                    if (input.getPrimarySet() && input.getPrimary()) {
                        if (!allowPrimary) {
                            throw XProcException.staticError(8, node, "The \"primary\" attribute is not allowed in this context.");
                        }

                        if (input.getParameterInput()) {
                            primaryParamInputCount++;
                            if (primaryParamInputCount > 1) {
                                throw XProcException.staticError(30, node, "You cannot have more than one primary parameter input port.");
                            }
                        } else {
                            primaryDocInputCount++;
                            if (primaryDocInputCount > 1) {
                                throw XProcException.staticError(30, node, "You cannot have more than one primary input port.");
                            }
                        }
                    }

                    input.setPosition(pos++);
                    if (step.getInput(input.getPort()) != null || step.getOutput(input.getPort()) != null) {
                        runtime.error(null, XProcException.staticError(11, node, "Duplicate port name: " + input.getPort()));
                    } else {
                        step.addInput(input);
                    }
                } else if (XProcConstants.p_output.equals(nodeName)) {
                    Output output = readOutput(step, node);

                    if (output.getPrimarySet() && output.getPrimary()) {
                        if (!allowPrimary) {
                            throw XProcException.staticError(8, node, "The \"primary\" attribute is not allowed in this context.");
                        }
                        primaryOutputCount++;
                        if (primaryOutputCount > 1) {
                            throw XProcException.staticError(14, node, "You cannot have more than one primary output port.");
                        }
                    }

                    // Output bindings are really *input* bindings. They're where the processor
                    // has to *read from* to get the output...
                    if (output.getBinding().size() != 0) {
                        Input input = new Input(runtime, output.getNode());
                        input.setPort("|" + output.getPort());
                        for (Binding binding : output.getBinding()) {
                            input.addBinding(binding);
                        }
                        output.clearBindings();

                        input.setPrimary(output.getPrimary());
                        input.setSequence(output.getSequence());
                        step.addInput(input);
                    }

                    if (step.getInput(output.getPort()) != null || step.getOutput(output.getPort()) != null) {
                        runtime.error(null, XProcException.staticError(11, node, "Duplicate port name: " + output.getPort()));
                    } else {
                        step.addOutput(output);
                    }
                } else if (XProcConstants.p_option.equals(nodeName)
                           || XProcConstants.p_with_option.equals(nodeName)) {

                    if (XProcConstants.p_pipeline.equals(step.getType())
                            || XProcConstants.p_declare_step.equals(step.getType())) {
                        if (XProcConstants.p_with_option.equals(nodeName)) {
                            throw XProcException.staticError(44, node, "Can't use p:with-option here.");
                        }
                    } else {
                        if (XProcConstants.p_option.equals(nodeName)) {
                            throw XProcException.staticError(44, node, "Can't use p:option here.");
                        }
                    }

                    Option option = readOption(step, node);
                    option.setStep(step);
                    step.addOption(option);
                } else if (XProcConstants.p_with_param.equals(nodeName)) {
                    Parameter param = readParameter(step, node);
                    param.setStep(step);
                    param.setPosition(pos++);
                    step.addParameter(param);
                } else if (XProcConstants.p_log.equals(nodeName)) {
                    Log log = readLog(node);
                    if (log.getPort() == null) {
                        throw XProcException.staticError(26, node, "A p:log must specify a port.");
                    }
                    if (log.getPort() != null && loggers.containsKey(log.getPort())) {
                        throw XProcException.staticError(26, node, "A p:log was specified more than once for the same port: " + log.getPort());
                    }
                    loggers.put(log.getPort(), log);
                } else if (XProcConstants.p_import.equals(nodeName)) {
                    rest.add(node);
                } else if (XProcConstants.p_serialization.equals(nodeName)) {
                    Serialization ser = readSerialization(node);
                    String port = ser.getPort();
                    if (port == null || serializations.containsKey(port)) {
                        throw XProcException.staticError(39, node, "A p:serialization must specify a port and can only be specified once.");
                    }

                    serializations.put(port, ser);
                } else {
                    throw XProcException.staticError(44, node, "Unexpected element: " + nodeName);
                }
            } else {
                if (node.getNodeKind() == XdmNodeKind.TEXT) {
                    throw XProcException.staticError(37, node, "Unexpected text: " + node.getStringValue());
                }

                done = true;
                rest.add(node);
            }
        }

        // Make sure single inputs and outputs are primary by default
        int inputCount = 0;
        Input maybePrimaryInput = null;
        for (Input input : step.inputs()) {
            if (!input.getParameterInput() && !input.getPort().startsWith("|")) {
                inputCount++;
                maybePrimaryInput = input;
            }
        }

        if (inputCount == 1 && !maybePrimaryInput.getPrimary() && !maybePrimaryInput.getPrimarySet()) {
            maybePrimaryInput.setPrimary(true);
        }

        inputCount = 0;
        maybePrimaryInput = null;
        for (Input input : step.inputs()) {
            if (input.getParameterInput() && !input.getPort().startsWith("|")) {
                inputCount++;
                maybePrimaryInput = input;
            }
        }

        if (inputCount == 1 && !maybePrimaryInput.getPrimary() && !maybePrimaryInput.getPrimarySet()) {
            maybePrimaryInput.setPrimary(true);
        }

        int outputCount = 0;
        Output maybePrimaryOutput = null;
        for (Output output : step.outputs()) {
            outputCount++;
            maybePrimaryOutput = output;
        }

        if (outputCount == 1 && !maybePrimaryOutput.getPrimary() && !maybePrimaryOutput.getPrimarySet()) {
            maybePrimaryOutput.setPrimary(true);
        }

        if (XProcConstants.p_declare_step.equals(step.getType())) {
            // We have to check for output serializations here.
            for (Output output : step.outputs()) {
                String port = output.getPort();
                if (serializations.containsKey(port)) {
                    Serialization serial = serializations.get(port);
                    output.setSerialization(serial);
                    serializations.remove(port);
                }
            }

            if (serializations.size() != 0) {
                // We know this is true here and now because p:pipeline can't have defaulted inputs/outputs
                throw XProcException.staticError(39, step.getNode(), "A p:serialization specifies a non-existant port.");
            }
        }

        for (String port : loggers.keySet()) {
            step.addLog(loggers.get(port));
        }

        boolean vars = false;
        for (XdmNode node : rest) {
            vars = vars || XProcConstants.p_variable.equals(node.getNodeName());
        }

        if (vars) {
            if (!allowVariables) {
                throw XProcException.staticError(44, step.getNode(), "Variables are not allowed here");
            }

            // p:declare-step/p:pipeline is a special case, see readDeclareStep
            if (!XProcConstants.p_pipeline.equals(step.getType())
                    && !XProcConstants.p_declare_step.equals(step.getType())) {
                while (rest.size() > 0 && XProcConstants.p_variable.equals(rest.get(0).getNodeName())) {
                    Variable var = readVariable(step, rest.remove(0));
                    step.addVariable(var);
                }
            }
        }

        return rest.size() == 0 ? null : rest;
    }

    private Input readInput(Step parent, XdmNode node) {
        QName nodeName = node.getNodeName();

        if (XProcConstants.p_input.equals(nodeName)) {
            checkAttributes(node, new String[] { "kind", "port", "primary", "sequence", "select" }, false);
        } else if (XProcConstants.p_iteration_source.equals(nodeName)) {
            checkAttributes(node, new String[] { "select" }, false);
        } else if (XProcConstants.p_viewport_source.equals(nodeName)
                   || XProcConstants.p_xpath_context.equals(nodeName)) {
            checkAttributes(node, null, false);
        } else {
            throw new UnsupportedOperationException("Unexpected name in readInput: " + nodeName);
        }

        String kind = node.getAttributeValue(new QName("kind"));
        String port = checkNCName(node.getAttributeValue(new QName("port")));
        String primary = node.getAttributeValue(new QName("primary"));
        String sequence = node.getAttributeValue(new QName("sequence"));
        String select = node.getAttributeValue(new QName("select"));

        if (port == null && XProcConstants.p_input.equals(node.getNodeName())) {
            throw XProcException.staticError(38, node, "You must specify a port name for all p:input ports.");
        }

        if (kind == null) {
            kind = "document";
        }

        if (!"document".equals(kind) && !"parameter".equals(kind)) {
            runtime.error(null, XProcException.staticError(33, node, "Kind must be document or parameter"));
        }

        if (primary != null &&  !"true".equals(primary) && !"false".equals(primary)) {
            runtime.error(null, XProcException.staticError(40, node, "Primary must be 'true' or 'false'"));
        }

        if (sequence != null) {
            if ("parameter".equals(kind)) {
                if (!"true".equals(sequence)) {
                    runtime.error(null, XProcException.staticError(40, node, "Sequence cannot be 'false' on a parameter input"));
                }
            } else {
                if (!"true".equals(sequence) && !"false".equals(sequence)) {
                    runtime.error(null, XProcException.staticError(40, node, "Sequence must be 'true' or 'false'"));
                }
            }
        }

        if (XProcConstants.p_iteration_source.equals(nodeName)) {
            port = "#iteration-source";
            sequence = "true";
            primary = "true";
        } else if (XProcConstants.p_viewport_source.equals(nodeName)) {
            port = "#viewport-source";
            sequence = "false";
            primary = "true";
        } else if (XProcConstants.p_xpath_context.equals(nodeName)) {
            port = "#xpath-context";
            sequence = "false";
            primary = "true";
        } else if (port == null) {
            throw XProcException.staticError(44, node, "No port name specified on input.");
        }

        Input input = new Input(runtime, node);
        input.setPort(port);
        input.setSequence(sequence);
        input.setPrimary(primary);

        if ("parameter".equals(kind)) {
            input.setParameterInput();
            input.setSequence(true);
        }

        input.setDebugReader(node.getAttributeValue(new QName(XProcConstants.NS_CALABASH_EX, "debug-reader")) != null);
        input.setDebugWriter(node.getAttributeValue(new QName(XProcConstants.NS_CALABASH_EX, "debug-writer")) != null);

        if (select != null) {
            input.setSelect(select);
        }

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            Binding binding = readBinding(parent, snode);
            if (binding != null) {
                input.addBinding(binding);
            }
        }

        checkExtensionAttributes(node, input);

        return input;
    }

    private Output readOutput(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "port", "primary", "sequence" }, false);

        String port = checkNCName(node.getAttributeValue(new QName("port")));

        if (port == null) {
            throw XProcException.staticError(38, node, "You must specify a port name for all p:output ports.");
        }

        String primary = node.getAttributeValue(new QName("primary"));
        String sequence = node.getAttributeValue(new QName("sequence"));

        Output output = new Output(runtime, node);
        output.setPort(port);
        output.setSequence(sequence);
        output.setPrimary(primary);

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            Binding binding = readBinding(parent, snode);
            if (binding != null) {
                output.addBinding(binding);
            }
        }

        checkExtensionAttributes(node, output);

        return output;
    }

    private Binding readBinding(Step parent, XdmNode node) {
        Binding binding = null;

        QName nodeName = node.getNodeName();
        if (XProcConstants.p_pipe.equals(nodeName)) {
            binding = readPipe(node);
        } else if (XProcConstants.p_document.equals(nodeName)) {
            binding = readDocument(node);
        } else if (XProcConstants.p_inline.equals(nodeName)) {
            binding = readInline(parent, node);
        } else if (XProcConstants.p_empty.equals(nodeName)) {
            binding = readEmpty(node);
        } else if (XProcConstants.p_data.equals(nodeName)) {
            binding = readData(node);
        } else {
            throw XProcException.staticError(44, node, "Unexpected in input: " + nodeName);
        }

        checkExtensionAttributes(node, binding);

        return binding;
    }

    private PipeNameBinding readPipe(XdmNode node) {
        checkAttributes(node, new String[] { "port", "step" }, false);

        String step = checkNCName(node.getAttributeValue(new QName("step")));
        String port = checkNCName(node.getAttributeValue(new QName("port")));

        if (step == null || port == null) {
            if (step == null) {
                throw XProcException.staticError(38, node, "Missing step attribute.");
            } else {
                throw XProcException.staticError(38, node, "Missing port attribute.");
            }
        }

        PipeNameBinding pipe = new PipeNameBinding(runtime, node);

        pipe.setStep(step);
        pipe.setPort(port);

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw new IllegalArgumentException("Unexpected in pipe: " + snode.getNodeName());
        }

        checkExtensionAttributes(node, pipe);

        return pipe;
    }

    private DocumentBinding readDocument(XdmNode node) {
        checkAttributes(node, new String[] { "href" }, false);

        String href = node.getAttributeValue(new QName("href"));

        DocumentBinding doc = new DocumentBinding(runtime, node);
        doc.setHref(href);

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw new IllegalArgumentException("Unexpected in document: " + snode.getNodeName());
        }

        checkExtensionAttributes(node, doc);

        return doc;
    }

    private DataBinding readData(XdmNode node) {
        checkAttributes(node, new String[] { "href", "wrapper", "wrapper-namespace", "wrapper-prefix", "content-type" }, false);

        String href = node.getAttributeValue(new QName("href"));
        String wrapstr = node.getAttributeValue(new QName("wrapper"));
        String wrappfx = node.getAttributeValue(new QName("wrapper-prefix"));
        String wrapns  = node.getAttributeValue(new QName("wrapper-namespace"));
        String contentType = node.getAttributeValue(new QName("content-type"));

        if (wrappfx != null && wrapns == null) {
            throw XProcException.dynamicError(34, node, "You cannot specify a prefix without a namespace.");
        }

        if (wrapns != null && wrapstr == null) {
            throw XProcException.dynamicError(34, node, "You cannot specify a namespace without a wrapper.");
        }

        if (wrapns != null && wrapstr != null && wrapstr.indexOf(":") >= 0) {
            throw XProcException.dynamicError(34, node, "You cannot specify a namespace if the wrapper name contains a colon.");
        }

        if (wrapns == null && wrapstr != null && wrapstr.indexOf(":") <= 0) {
            throw XProcException.dynamicError(25, node, "FIXME: what error is this?");
        }

        DataBinding doc = new DataBinding(runtime, node);
        doc.setHref(href); // FIXME: what about making it absolute?

        if (wrapstr != null) {
            if (wrapstr.indexOf(":") > 0) {
                doc.setWrapper(new QName(wrapstr, node));
            } else if (wrappfx != null) {
                doc.setWrapper(new QName(wrappfx, wrapns, wrapstr));
            } else {
                doc.setWrapper(new QName(wrapns, wrapstr));
            }
        }

        if (contentType != null) {
            doc.setContentType(contentType);
        }

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw new IllegalArgumentException("Unexpected in document: " + snode.getNodeName());
        }

        checkExtensionAttributes(node, doc);

        return doc;
    }

    private EmptyBinding readEmpty(XdmNode node) {
        checkAttributes(node, new String[] { }, false);

        EmptyBinding empty = new EmptyBinding(runtime, node);

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw new IllegalArgumentException("Unexpected in empty: " + snode.getNodeName());
        }

        checkExtensionAttributes(node, empty);

        return empty;
    }

    private InlineBinding readInline(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "exclude-inline-prefixes" }, false);

        InlineBinding inline = new InlineBinding(runtime, node);

        boolean seenelem = false;
        XdmSequenceIterator iter = node.axisIterator(Axis.CHILD);
        while (iter.hasNext()) {
            XdmNode child = (XdmNode) iter.next();
            if (child.getNodeKind() == XdmNodeKind.ELEMENT) {
                if (seenelem) {
                    throw new IllegalArgumentException("Not a well-formed inline document");
                }
                seenelem = true;
            }
            inline.addNode(child);
        }

        HashSet excludeURIs = S9apiUtils.excludeInlinePrefixes(node, node.getAttributeValue(_exclude_inline_prefixes));
        while (!(parent instanceof DeclareStep)) {
            parent = parent.parent;
        }

        if (parent instanceof DeclareStep) {
            HashSet excluded = ((DeclareStep) parent).getExcludeInlineNamespaces();
            if (excluded != null) {
                for (String uri : excluded) {
                    excludeURIs.add(uri);
                }
            }
        } else {
            throw new UnsupportedOperationException("This can't happen: parent of inline is not a step!?");
        }

        checkExtensionAttributes(node, inline);

        inline.excludeNamespaces(excludeURIs);

        return inline;
    }

    private Option readOption(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "name", "required", "select" }, false);

        String name = node.getAttributeValue(new QName("name"));
        String required = node.getAttributeValue(new QName("required"));
        String select = node.getAttributeValue(new QName("select"));
        String type = node.getAttributeValue(XProcConstants.cx_type);
        String sequenceType = node.getAttributeValue(XProcConstants.cx_as);

        if (name == null) {
            throw XProcException.staticError(38, node, "Attribute \"name\" required on p:with-option");
        }

        QName oname;
        if (name.contains(":")) {
            oname = new QName(name, node);
        } else {
            oname = new QName(name);
        }

        if (XProcConstants.NS_XPROC.equals(oname.getNamespaceURI())) {
            throw XProcException.staticError(28, node, "You cannot specify an option in the p: namespace.");
        }

        if (required != null && !"false".equals(required) && !"true".equals(required)) {
            throw XProcException.staticError(19, node, "The required attribute must be 'true' or 'false'.");
        }

        Option option = new Option(runtime, node);
        option.setName(oname);
        option.setRequired(required);
        option.setSelect(select);
        if (type != null) {
            if (type.contains(":"))
                try {
                    option.setType(type, new QName(type, node));
                } catch (IllegalArgumentException e) {
                    throw new XProcException(
                        new RuntimeException("Cannot parse type (\"type\" attribute) on " + node, e));
                }
            else
                option.setType(type);
        }
        if (runtime.getAllowGeneralExpressions()) {
            try {
                if (sequenceType != null) {
                    option.setSequenceType(SequenceType.parse(sequenceType, node));
                } else if (type != null) {
                    if (type.contains(":"))
                        option.setSequenceType(SequenceType.parse(type, node));
                    else
                        option.setSequenceType(SequenceType.XS_STRING);
                }
            } catch (IllegalArgumentException e) {
                throw new XProcException(
                    new RuntimeException("Cannot parse type (\"as\" attribute) on " + node, e));
            }
        }

        readNamespaceBindings(parent, option, node, select);

        checkExtensionAttributes(node, option);

        return option;
    }

    private Parameter readParameter(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "port", "name", "select" }, false);

        String name = node.getAttributeValue(new QName("name"));
        String select = node.getAttributeValue(new QName("select"));
        String port = checkNCName(node.getAttributeValue(new QName("port")));

        if (name == null) {
            runtime.error(null, XProcException.staticError(38, node, "Attribute \"name\" required on p:with-param"));
        }

        Parameter parameter = new Parameter(runtime, node);
        parameter.setPort(port);

        // If the name contains a colon, get the namespace from the node, otherwise it's in no namespace
        if (name.contains(":")) {
            parameter.setName(new QName(name, node));
        } else {
            parameter.setName(new QName("", name));
        }

        parameter.setSelect(select);

        readNamespaceBindings(parent, parameter, node, select);

        checkExtensionAttributes(node, parameter);

        return parameter;
    }

    private Variable readVariable(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "name", "select" }, false);

        String name = node.getAttributeValue(new QName("name"));
        String select = node.getAttributeValue(new QName("select"));
        String sequenceType = node.getAttributeValue(XProcConstants.cx_as);

        QName oname = new QName(name, node);

        // If it has no prefix, then it's in no namespace, not the default namespace
        if (oname.getPrefix() == null || "".equals(oname.getPrefix())) {
            oname = new QName("", name);
        }

        if (XProcConstants.NS_XPROC.equals(oname.getNamespaceURI())) {
            throw XProcException.staticError(28, node, "You cannot specify a variable in the p: namespace.");
        }

        Variable variable = new Variable(runtime, node);
        variable.setName(oname);
        variable.setSelect(select);
        if (runtime.getAllowGeneralExpressions() && sequenceType != null) {
            try {
                variable.setSequenceType(SequenceType.parse(sequenceType, node));
            } catch (IllegalArgumentException e) {
                throw new XProcException(
                    new RuntimeException("Cannot parse type (\"as\" attribute) on " + node, e));
            }
        }

        readNamespaceBindings(parent, variable, node, select);

        checkExtensionAttributes(node, variable);

        return variable;
    }

    private void readNamespaceBindings(Step parent, EndPoint endpoint, XdmNode node, String select) {
        boolean hadNamespaceBinding = false;
        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            QName nodeName = snode.getNodeName();

            if (XProcConstants.p_namespaces.equals(nodeName)) {
                hadNamespaceBinding = true;
                NamespaceBinding nsbinding = new NamespaceBinding(runtime, snode);
                checkAttributes(snode, new String[]{"binding", "element", "except-prefixes"}, false);

                String value = snode.getAttributeValue(new QName("binding"));
                if (value != null) {
                    nsbinding.setBinding(value);
                }

                value = snode.getAttributeValue(new QName("element"));
                if (value != null) {
                    nsbinding.setXPath(value);
                }

                value = snode.getAttributeValue(new QName("except-prefixes"));
                if (value != null) {
                    for (String pfx : value.split("\\s+")) {
                        // Is this really the best way?
                        try {
                            QName n = new QName(pfx+":localName",snode);
                            nsbinding.addExcludedNamespace(n.getNamespaceURI());
                        } catch (IllegalArgumentException iae) {
                            // Bad prefix
                            throw XProcException.staticError(51, node, "Unbound prefix in except-prefixes: " + pfx);
                        }
                    }
                }

                // FIXME: ? HACK!
                ((ComputableValue) endpoint).addNamespaceBinding(nsbinding);

                for (XdmNode tnode : new AxisNodes(runtime, snode, Axis.CHILD, AxisNodes.PIPELINE)) {
                    throw XProcException.staticError(44, snode, "p:namespaces must be empty");
                }
            } else {
                Binding binding = readBinding(parent, snode);
                if (binding != null) {
                    if (XProcConstants.p_option.equals(node.getNodeName())) {
                        throw XProcException.staticError(44, node, "No bindings allowed.");
                    }
                    endpoint.addBinding(binding);
                }
            }
        }

        if (!hadNamespaceBinding) {
            if (select != null && select.matches("^\\$[a-zA-Z_][-a-zA-Z0-9_]*$")) {
                String varname = select.substring(1);
                // FIXME: need to work out the nsbindings later
            }
            // Hack: Rely on the fact that getVariables don't have binding, element, or except-prefixes attributes...
            NamespaceBinding nsbinding = new NamespaceBinding(runtime, node);
            ((ComputableValue) endpoint).addNamespaceBinding(nsbinding);
        }
    }

    private Serialization readSerialization(XdmNode node) {
        checkAttributes(node, new String[] { "port", "byte-order-mark", "cdata-section-elements",
                "doctype-public", "doctype-system", "encoding", "escape-uri-attributes",
                "include-content-type", "indent", "media-type", "method", "normalization-form",
                "omit-xml-declaration", "standalone", "undeclare-prefixes", "version"}, false);

        Serialization serial = new Serialization(runtime, node);

        String value = node.getAttributeValue(new QName("port"));
        serial.setPort(value);

        value = node.getAttributeValue(new QName("byte-order-mark"));
        if (value != null) {
            checkBoolean(node, "byte-order-mark", value);
            serial.setByteOrderMark("true".equals(value));
        }

        value = node.getAttributeValue(new QName("cdata-section-elements"));
        if (value != null) {
            throw new UnsupportedOperationException("cdata-section-elements not yet supported");
        }

        value = node.getAttributeValue(new QName("doctype-public"));
        serial.setDoctypePublic(value);

        value = node.getAttributeValue(new QName("doctype-system"));
        serial.setDoctypeSystem(value);

        value = node.getAttributeValue(new QName("encoding"));
        serial.setEncoding(value);

        value = node.getAttributeValue(new QName("escape-uri-attributes"));
        if (value != null) {
            checkBoolean(node, "escape-uri-attributes", value);
            serial.setEscapeURIAttributes("true".equals(value));
        }

        value = node.getAttributeValue(new QName("include-content-type"));
        if (value != null) {
            checkBoolean(node, "include-content-type", value);
            serial.setIncludeContentType("true".equals(value));
        }

        value = node.getAttributeValue(new QName("indent"));
        if (value != null) {
            checkBoolean(node, "indent", value);
            serial.setIndent("true".equals(value));
        }

        value = node.getAttributeValue(new QName("media-type"));
        serial.setMediaType(value);

        value = node.getAttributeValue(new QName("method"));
        if (value != null) {
            QName name = null;
            if (value.contains(":")) {
                name = new QName(value, node);
            } else {
                name = new QName("", value);
            }
            if ("".equals(name.getPrefix())) {
                String method = name.getLocalName();
                if ("html".equals(method) || "xhtml".equals(method) || "text".equals(method) || "xml".equals(method)) {
                    serial.setMethod(name);
                } else {
                    runtime.error(
                        null,
                        XProcException.stepError(
                            1, node, "Only the xml, xhtml, html, and text serialization methods are supported."));
                }
            } else {
                runtime.error(
                    null,
                    XProcException.stepError(
                        1, node, "Only the xml, xhtml, html, and text serialization methods are supported."));
            }
        }

        value = node.getAttributeValue(new QName("normalization-form"));
        serial.setNormalizationForm(value);

        value = node.getAttributeValue(new QName("omit-xml-declaration"));
        if (value != null) {
            checkBoolean(node, "omit-xml-declaration", value);
            serial.setOmitXMLDeclaration("true".equals(value));
        }

        value = node.getAttributeValue(new QName("standalone"));
        serial.setStandalone(value);

        value = node.getAttributeValue(new QName("undeclare-prefixes"));
        if (value != null) {
            checkBoolean(node, "undeclare-prefixes", value);
            serial.setUndeclarePrefixes("true".equals(value));
        }

        value = node.getAttributeValue(new QName("version"));
        serial.setVersion(value);

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw XProcException.staticError(44, node, "p:serialization must be empty.");
        }

        checkExtensionAttributes(node, serial);

        return serial;
    }

    private void checkBoolean(XdmNode node, String name, String value) {
        if (value != null && !"true".equals(value) && !"false".equals(value)) {
            runtime.error(null, XProcException.staticError(40, node, name + " on serialization must be 'true' or 'false'"));
        }
    }

    private Log readLog(XdmNode node) {
        checkAttributes(node, new String[] { "port", "href" }, false);
        String port = checkNCName(node.getAttributeValue(new QName("port")));
        String href = node.getAttributeValue(new QName("href"));

        URI hrefURI = null;
        if (href != null) {
            hrefURI = node.getBaseURI().resolve(href);
        }

        Log log = new Log(runtime, node);
        log.setPort(port);
        log.setHref(hrefURI);

        checkExtensionAttributes(node, log);

        for (XdmNode snode : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw XProcException.staticError(44, node, "p:log must be empty");
        }

        checkExtensionAttributes(node, log);

        return log;
    }

    // FIXME: parent and declScope arguments are always equal: simplify!
    private Step readStep(Step parent, DeclarationScope declScope, XdmNode node) {
        QName stepType = node.getNodeName();

        if (XProcConstants.p_declare_step.equals(stepType)
                || XProcConstants.p_pipeline.equals(stepType)) {
            return readDeclareStep(node, declScope);
        } else if (XProcConstants.p_import.equals(stepType)) {
            return readImport(node);
        } else if (XProcConstants.p_for_each.equals(stepType)) {
            return readForEach(parent, node);
        } else if (XProcConstants.p_viewport.equals(stepType)) {
            return readViewport(parent, node);
        } else if (XProcConstants.p_choose.equals(stepType)) {
            return readChoose(parent, node);
        } else if (XProcConstants.p_when.equals(stepType)) {
            return readWhen(parent, node);
        } else if (XProcConstants.p_otherwise.equals(stepType)) {
            return readOtherwise(parent, node);
        } else if (XProcConstants.p_group.equals(stepType)) {
            return readGroup(parent, node);
        } else if (XProcConstants.p_try.equals(stepType)) {
            return readTry(parent, node);
        } else if (XProcConstants.p_catch.equals(stepType)) {
            return readCatch(parent, node);
        } else if (XProcConstants.cx_until_unchanged.equals(stepType)) {
            return readUntilUnchanged(parent, node);
        }

        DeclareStep decl = declScope.getDeclaration(stepType);
        if (decl == null) {
            throw XProcException.staticError(44, node, "Unexpected step name: " + stepType);
        }

        // Must be an atomic step in a subpipeline
        checkAttributes(node, new String[] { "name" }, true);

        String stepName = checkNCName(node.getAttributeValue(new QName("name")));

        Step step = new Step(runtime, node, stepType, stepName);
        step.setDeclaration(decl);
        step.parent = parent;

        boolean pStep = XProcConstants.NS_XPROC.equals(node.getNodeName().getNamespaceURI());

        // Store extension attributes and convert any option shortcut attributes into options
        for (XdmNode attr : new AxisNodes(node, Axis.ATTRIBUTE)) {
            QName aname = attr.getNodeName();

            if ((pStep && aname.equals(_use_when))
                || (!pStep && aname.equals(p_use_when))) {
                continue;
            }

            if (XMLConstants.NULL_NS_URI.equals(aname.getNamespaceURI())) {
                if (!"name".equals(aname.getLocalName())) {
                    Option option = new Option(runtime, node);
                    option.setName(new QName("", aname.getLocalName()));
                    option.setSelect("'" + attr.getStringValue().replace("'", "''") + "'");
                    option.addNamespaceBinding(new NamespaceBinding(step.getXProc(),node));

                    step.addOption(option);
                }
            } else {
                step.addExtensionAttribute(attr);
            }
        }

        Vector rest = readSignature(step);
        if (rest != null) {
            String message = "A " + stepType + " step must contain only a signature.";
            if (XProcConstants.p_option.equals(rest.get(0).getNodeName())) {
                message += " p:option is not allowed, did you mean p:with-option instead?";
            } else if (XProcConstants.p_parameter.equals(rest.get(0).getNodeName())) {
                message += " p:parameter is not allowed, did you mean p:with-param instead?";
            } else {
                message += " " + rest.get(0).getNodeName() + " not allowed.";
            }
            throw XProcException.staticError(44, rest.get(0), message);
        }

        return step;
    }

    private DeclareStep readDeclareStep(XdmNode node, DeclarationScope declScope) {
        QName name = node.getNodeName();

        if (!name.equals(XProcConstants.p_declare_step) && !name.equals(XProcConstants.p_pipeline)) {
            throw XProcException.staticError(59, node, "Expected p:declare-step or p:pipeline, got " + name);
        }

        checkAttributes(node, new String[] { "type", "name", "version", "psvi-required", "xpath-version", "exclude-inline-prefixes"}, false);

        String stepName = checkNCName(node.getAttributeValue(_name));
        String typeName = node.getAttributeValue(_type);

        QName type = null;
        if (typeName == null) {
            type = TypeUtils.generateUniqueType();
        } else {
            if (typeName.indexOf(":") <= 0) {
                throw XProcException.staticError(25, node, "Type must be in a namespace.");
            }
            type = new QName(typeName, node);

            if (!loadingStandardLibrary && XProcConstants.NS_XPROC.equals(type.getNamespaceURI())) {
                throw XProcException.staticError(25, node, "Type cannot be in the p: namespace.");
            }
        }

        DeclareStep step = new DeclareStep(runtime, node, stepName);
        step.setVersion(inheritedVersion(node));

        boolean psviRequired = booleanAttr(node.getAttributeValue(new QName("psvi-required")));
        String xpathVersion = node.getAttributeValue(new QName("xpath-version"));

        if ("1.0".equals(xpathVersion)) {
            // FIXME: Warn about v2!
        } else if (xpathVersion != null && !"2.0".equals(xpathVersion)) {
            throw XProcException.dynamicError(27, node, "XPath version must be 1.0 or 2.0.");
        }

        // Store extension attributes and convert any option shortcut attributes into options
        for (XdmNode attr : new AxisNodes(node, Axis.ATTRIBUTE)) {
            QName aname = attr.getNodeName();
            if (XMLConstants.NULL_NS_URI.equals(aname.getNamespaceURI())) {
                if (!"type".equals(aname.getLocalName()) && !"name".equals(aname.getLocalName())
                        && !"version".equals(aname.getLocalName()) && !"use-when".equals(aname.getLocalName())
                        && !"psvi-required".equals(aname.getLocalName()) && !"xpath-version".equals(aname.getLocalName())
                        && !"exclude-inline-prefixes".equals(aname.getLocalName())) {
                    throw XProcException.staticError(44, node, "Attribute not allowed: " + aname.getLocalName());
                }
            } else {
                step.addExtensionAttribute(attr);
            }
        }

        step.setDeclaredType(type);
        step.setPsviRequired(psviRequired);
        step.setXPathVersion(xpathVersion);

        HashSet excludeURIs = S9apiUtils.excludeInlinePrefixes(node, node.getAttributeValue(_exclude_inline_prefixes));
        // if parent is a p:library, p:declare-step or p:pipeline, get the exclusions from there too...
        if (S9apiUtils.getParent(node) != null) {
            XdmNode parent = node.getParent();
            if (parent.getAttributeValue(_exclude_inline_prefixes) != null
                && (XProcConstants.p_library.equals(parent.getNodeName())
                    || XProcConstants.p_pipeline.equals(parent.getNodeName())
                    || XProcConstants.p_declare_step.equals(parent.getNodeName()))) {
                HashSet pexcl = S9apiUtils.excludeInlinePrefixes(parent, parent.getAttributeValue(_exclude_inline_prefixes));
                for (String uri : pexcl) {
                    excludeURIs.add(uri);
                }
            }
        }

        step.setExcludeInlineNamespaces(excludeURIs);

        if (name.equals(XProcConstants.p_pipeline)) {
            Input input = new Input(runtime, node);
            input.setPort("source");
            input.setPrimary(true);
            input.setSequence(false);
            step.addInput(input);

            input = new Input(runtime, node);
            input.setPort("parameters");
            input.setParameterInput(true);
            input.setPrimary(true);
            input.setSequence(true);
            step.addInput(input);

            Output output = new Output(runtime, node);
            output.setPort("result");
            output.setPrimary(true);
            output.setSequence(false);
            step.addOutput(output);
        }

        Vector rest = readSignature(step);
        step.setAtomic(rest == null);

        step.setParentScope(declScope);
        declScope.declareStep(step.getDeclaredType(), step);

        // Check that we have legitimate bindings
        for (Input input : step.inputs()) {
            if (step.isAtomic()) {
                if (input.getBinding().size() != 0) {
                    runtime.error(
                        null,
                        XProcException.staticError(42, input.getNode(), "Input bindings are not allowed on an atomic step"));
                }
            } else {
                if (!input.getPort().startsWith("|")) {
                    for (Binding binding : input.getBinding()) {
                        if (binding.getBindingType() == Binding.PIPE_NAME_BINDING) {
                            runtime.error(
                                null,
                                XProcException.staticError(40, input.getNode(), "Default input bindings cannot use p:pipe"));
                        }
                    }
                }
            }
        }

        // No output bindings are allowed!
        for (Output output : step.outputs()) {
            Input input = step.getInput("|" + output.getPort());
            if (step.isAtomic() && input != null) {
                runtime.error(
                    null,
                    XProcException.staticError(29, output.getNode(), "Output bindings are not allowed on an atomic step"));
            }
        }

        // make XSLT functions available in child p:declare-step
        if (declScope instanceof DeclareStep) {
            for (XdmNode n : ((DeclareStep)declScope).getXsltFunctionImports()) {
                step.addXsltFunctionImport(n);
            }
        }

        Vector steps = new Vector();

        if (rest != null) {
            for (XdmNode substepNode : rest) {
                if (XProcConstants.p_variable.equals(substepNode.getNodeName())) {
                    Variable var = readVariable(step, substepNode);
                    step.addVariable(var);
                } else if (cx_import.equals(substepNode.getNodeName())) {
                    importFunctions(step, substepNode);
                } else {
                    if ((XProcConstants.p_declare_step.equals(substepNode.getNodeName()))
                            || XProcConstants.p_pipeline.equals(substepNode.getNodeName())) {
                        DeclareStep dstep = (DeclareStep) readStep(step, step, substepNode);
                        // It's not really part of the pipeline, but we need to parse it
                        // to make sure it gets added to the available steps
                    } else if (XProcConstants.p_import.equals(substepNode.getNodeName())) {
                        Import importElem = (Import) readStep(step, step, substepNode);
                        XdmNode root = importElem.getRoot();
                        PipelineLibrary lib = readLibrary(root, importElem.getHref());
                        if (lib != null)
                            step.addImport(lib);
                    } else {
                        steps.add(substepNode);
                    }
                }
            }

            step.checkPrimaryIO();
            rest = steps;
        }

        step.setXmlContent(rest);

        return step;
    }

    private List isDeclareStepBodyParsed = new ArrayList<>();

    private void parseDeclareStepBodyPassTwo(DeclareStep step) {

        if (isDeclareStepBodyParsed.contains(step))
            return;
        isDeclareStepBodyParsed.add(step);

        for (QName type : step.getInScopeTypes()) {
            parseDeclareStepBodyPassTwo(step.getDeclaration(type));
        }

        Vector rest = step.getXmlContent();

        if (rest != null) {
            for (XdmNode substepNode : rest) {
                Step substep = readStep(step, step, substepNode);
                step.addStep(substep);
            }
        }
    }

    private Double inheritedVersion(XdmNode node) {
        XdmNode parent = S9apiUtils.getParent(node);

        if (XProcConstants.p_declare_step.equals(node.getNodeName())
                || XProcConstants.p_pipeline.equals(node.getNodeName())
                || XProcConstants.p_library.equals(node.getNodeName())) {
            String version = node.getAttributeValue(_version);
            if (version != null) {
                TypeUtils.checkType(runtime, null, version, XProcConstants.xs_decimal, node, err_XS0063);
                return Double.parseDouble(version);
            }
        }

        if (parent == null) {
            throw XProcException.staticError(62, node, "Version attribute is required.");
        } else {
            return inheritedVersion(parent);
        }
    }

    private Import readImport(XdmNode node) {
        checkAttributes(node, new String[] { "href" }, false);

        String href = node.getAttributeValue(_href);
        URI importURI = node.getBaseURI().resolve(href);

        for (XdmNode child : new AxisNodes(runtime, node, Axis.CHILD, AxisNodes.PIPELINE)) {
            throw new UnsupportedOperationException("p:import must be empty.");
        }

        Import importElem = new Import(runtime, node);

        XdmNode doc;
        try {
            if (importURI.toASCIIString().equals(XProcConstants.CALABASH_EXTENSION_LIBRARY_1_0)) {
                doc = loadExtensionLibrary();
            } else {
                doc = runtime.parse(href, node.getBaseURI().toASCIIString());
            }
        } catch (XProcException xe) {
            if (XProcConstants.dynamicError(11).equals(xe.getErrorCode())) {
                throw XProcException.staticError(
                    52, node,
                    new RuntimeException("Cannot import: " + importURI.toASCIIString(), xe.getCause()));
            } else {
                throw xe;
            }
        } catch (Exception e) {
            throw new XProcException(e);
        }

        importURI = doc.getBaseURI();
        importElem.setHref(importURI);
        importElem.setRoot(S9apiUtils.getDocumentElement(doc));
        checkExtensionAttributes(node, importElem);

        return importElem;
    }

    private ForEach readForEach(Step parent, XdmNode node) {
        QName name = node.getNodeName();
        if (!XProcConstants.p_for_each.equals(name)) {
            throw new UnsupportedOperationException("Can't parse " + name + " as a pipeline!");
        }

        checkAttributes(node, new String[] { "name" }, false);

        String stepName = checkNCName(node.getAttributeValue(_name));

        ForEach step = new ForEach(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        // FIXME: Do I really need parent?
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:for-each must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }


    private UntilUnchanged readUntilUnchanged(Step parent, XdmNode node) {
        QName name = node.getNodeName();
        if (!XProcConstants.cx_until_unchanged.equals(name)) {
            throw new UnsupportedOperationException("Can't parse " + name + " as a cx:until-unchanged!");
        }

        checkAttributes(node, new String[] { "name" }, false);

        String stepName = checkNCName(node.getAttributeValue(_name));

        UntilUnchanged step = new UntilUnchanged(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A cx:until-unchanged must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }

    private Viewport readViewport(Step parent, XdmNode node) {
        QName name = node.getNodeName();
        if (!XProcConstants.p_viewport.equals(name)) {
            throw new UnsupportedOperationException("Can't parse " + name + " as a pipeline!");
        }

        checkAttributes(node, new String[] { "name", "match" }, false);

        String stepName = checkNCName(node.getAttributeValue(_name));
        RuntimeValue match = new RuntimeValue(node.getAttributeValue(new QName("match")), node);

        Viewport step = new Viewport(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        step.setMatch(match);

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:viewport must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }

    private Choose readChoose(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "name" }, false);
        String stepName = checkNCName(node.getAttributeValue(_name));

        Choose step = new Choose(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:choose must contain at least one p:when.");
        }

        for (XdmNode child : rest) {
            if (XProcConstants.p_when.equals(child.getNodeName())) {
                When substep = readWhen(step, child);
                step.addStep(substep);
            } else if (XProcConstants.p_otherwise.equals(child.getNodeName())) {
                Otherwise substep = readOtherwise(step, child);
                step.addStep(substep);
            } else {
                throw new UnsupportedOperationException("Not valid in a choose: " + child.getNodeName());
            }
        }

        step.checkPrimaryIO();
        return step;
    }

    private When readWhen(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "test" }, false);

        String stepName = checkNCName(node.getAttributeValue(px_name));
        String testExpr = node.getAttributeValue(new QName("test"));

        When step = new When(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setTest(testExpr);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:when must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }

    private Otherwise readOtherwise(Step parent, XdmNode node) {
        checkAttributes(node, null, false);

        String stepName = checkNCName(node.getAttributeValue(px_name));

        Otherwise step = new Otherwise(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:otherwise must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }

    private Group readGroup(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "name" }, false);

        String stepName = checkNCName(node.getAttributeValue(_name));

        Group step = new Group(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:group must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }

    private Try readTry(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "name" }, false);

        String stepName = checkNCName(node.getAttributeValue(_name));

        Try step = new Try(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:try must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();
        return step;
    }

    private Catch readCatch(Step parent, XdmNode node) {
        checkAttributes(node, new String[] { "name" }, false);

        String stepName = checkNCName(node.getAttributeValue(_name));

        Catch step = new Catch(runtime, node, stepName);
        checkExtensionAttributes(node, step);
        step.setParentScope((DeclareStep) parent);
        step.parent = parent;

        Vector rest = readSignature(step);

        Input input = new Input(runtime, step.getNode());
        input.setPort("error");
        input.addBinding(new ErrorBinding(runtime, step.getNode()));
        input.setPrimary(false);
        input.setSequence(true);
        step.addInput(input);

        if (rest == null) {
            throw XProcException.staticError(15, node, "A p:catch must contain a subpipeline.");
        }

        for (XdmNode substepNode : rest) {
            Step substep = readStep(step, step, substepNode);
            step.addStep(substep);
        }

        step.checkPrimaryIO();

        return step;
    }

    // ================================================================================================

    private HashSet checkAttributes(XdmNode node, String[] attrs, boolean optionShortcutsOk) {
        HashSet hash = null;
        if (attrs != null) {
            hash = new HashSet ();
            for (String attr : attrs) {
                hash.add(attr);
            }
        }
        HashSet options = null;

        Double version = inheritedVersion(node);

        for (XdmNode attr : new AxisNodes(node, Axis.ATTRIBUTE)) {
            QName aname = attr.getNodeName();

            // The use-when attribute is always ok
            if ((XProcConstants.NS_XPROC.equals(node.getNodeName().getNamespaceURI())
                    && aname.equals(_use_when))
                || (!XProcConstants.NS_XPROC.equals(node.getNodeName().getNamespaceURI())
                    && aname.equals(p_use_when))) {
                continue;
            }

            if ("".equals(aname.getNamespaceURI())) {
                if (hash.contains(aname.getLocalName())) {
                // ok
                } else if (optionShortcutsOk) {
                    if (options == null) {
                        options = new HashSet ();
                    }
                    options.add(aname.getLocalName());
                } else if (version > 1.0) {
                    // Ok, then, we'll just ignore it...
                } else {
                    runtime.error(
                        null,
                        XProcException.staticError(8, node, "Attribute \"" + aname + "\" not allowed on " + node.getNodeName()));
                }
            } else if (XProcConstants.NS_XPROC.equals(aname.getNamespaceURI())) {
                runtime.error(
                    null,
                    XProcException.staticError(8, node, "Attribute \"" + aname + "\" not allowed on " + node.getNodeName()));
                return null;
            }
            // Everything else is ok
        }

        return options;
    }

    // Can't be used for steps because it doesn't handle option shortcut attributes
    private void checkExtensionAttributes(XdmNode node, SourceArtifact src) {
        for (XdmNode attr : new AxisNodes(node, Axis.ATTRIBUTE)) {
            QName aname = attr.getNodeName();
            if ("".equals(aname.getNamespaceURI())) {
                // nop
            } else if (XProcConstants.NS_XPROC.equals(aname.getNamespaceURI())) {
                runtime.error(
                    null,
                    XProcException.staticError(8, node, "Attribute \"" + aname + "\" not allowed on " + node.getNodeName()));
            } else {
                src.addExtensionAttribute(attr);
            }
        }
    }

    private boolean booleanAttr(String value) {
        return booleanAttr(value, false);
    }

    private boolean booleanAttr(String value, boolean defval) {
        if (value == null) {
            return defval;
        }
        if ("true".equals(value)) {
            return true;
        }
        if ("false".equals(value)) {
            return false;
        }
        throw new IllegalArgumentException("Boolean value must be 'true' or 'false'.");
    }

    private String checkNCName(String name) {
        if (name != null) {
            try {
                TypeUtils.checkType(runtime, null, name, XProcConstants.xs_NCName, null);
            } catch (XProcException xe) {
                throw new XProcException(
                    new RuntimeException("Invalid name: \"" + name + "\". Step and port names must be NCNames.",
                                         xe.getCause()));
            }
        }
        return name;
    }

    private void importFunctions(DeclareStep step, XdmNode node) {
        String href = node.getAttributeValue(_href);
        String ns = node.getAttributeValue(_namespace);
        String type = node.getAttributeValue(_type);
        Processor processor = runtime.getProcessor();

        String sed = processor.getUnderlyingConfiguration().getEditionCode();

        if (type.contains("xsl")
            // This alternative implementation for XSLT functions does not depend on Saxon EE. We
            // use it only when we're running Saxon HE, even though it works for all editions of
            // Saxon.
            && "HE".equals(sed)) {
            try {
                // Note that this implementation behaves like p:import-functions from XProc 3, in
                // the sense that the loaded functions are not globally available but only within
                // the scope of the step declaration.
                step.addXsltFunctionImport(runtime.parse(href, node.getBaseURI().toASCIIString()));
                return;
            } catch (Exception e) {
                throw new XProcException(e);
            }
        }

        if (!"EE".equals(sed)) {
            throw new XProcException("Importing functions is only supported by Saxon EE.");
        }

        try {
            URL url = runtime.getStaticBaseURI().resolve(href).toURL();
            URLConnection connection = url.openConnection();
            FunctionLibrary fl = null;

            if (type.contains("xsl")) {
                SAXSource stylesheet = new SAXSource(new InputSource(connection.getInputStream()));
                XsltCompiler compiler = processor.newXsltCompiler();
                XsltExecutable exec = compiler.compile(stylesheet);
                PreparedStylesheet ps = exec.getUnderlyingCompiledStylesheet();
                fl = ps.getFunctionLibrary();
            } else {
                XQueryCompiler xqcomp = processor.newXQueryCompiler();
                StaticQueryContext sqc = xqcomp.getUnderlyingStaticContext();
                sqc.compileLibrary(connection.getInputStream(), "utf-8");
                XQueryExpression xqe = sqc.compileQuery("import module namespace f='" + ns + "'; .");
                QueryModule qm = xqe.getMainModule();
                fl = qm.getGlobalFunctionLibrary();
            }

            // We think this will work because we know from the test above that we're not in Saxon HE.
            // It's a bit fragile, but I can't think of a better way.
            Class pc = Class.forName("com.saxonica.config.ProfessionalConfiguration");
            Class fc = Class.forName("net.sf.saxon.functions.FunctionLibrary");
            Method setBinder = pc.getMethod("setExtensionBinder", String.class, fc);
            setBinder.invoke(processor.getUnderlyingConfiguration(), "xmlcalabash" + importCount, fl);
            importCount++;
        } catch (Exception e) {
            throw new XProcException(e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy