org.orbeon.oxf.processor.PageFlowControllerProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.processor.PageFlowControllerProcessor.java

Source

/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor;

import org.apache.log4j.Logger;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.QName;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.processor.pipeline.PipelineConfig;
import org.orbeon.oxf.processor.pipeline.PipelineProcessor;
import org.orbeon.oxf.processor.pipeline.ast.*;
import org.orbeon.oxf.processor.serializer.legacy.HTMLSerializer;
import org.orbeon.oxf.resources.URLFactory;
import org.orbeon.oxf.transformer.xupdate.XUpdateConstants;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.util.URLRewriterUtils;
import org.orbeon.oxf.xforms.XFormsConstants;
import org.orbeon.oxf.xml.NamespaceMapping;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.dom4j.*;

import java.net.MalformedURLException;
import java.util.*;

public class PageFlowControllerProcessor extends ProcessorImpl {

    static private Logger logger = LoggerFactory.createLogger(PageFlowControllerProcessor.class);

    public final static String INPUT_CONTROLLER = "controller";
    public final static String CONTROLLER_NAMESPACE_URI = "http://www.orbeon.com/oxf/controller";
    private final static Document TRUE_DOCUMENT = new NonLazyUserDataDocument();
    private final static Document FALSE_DOCUMENT = new NonLazyUserDataDocument();
    private final static NamespaceMapping NAMESPACES_WITH_XSI_AND_XSLT;
    public final static String EXTRACT_INSTANCE_XPATH = "/*/*[local-name() = 'instance' and namespace-uri() = '"
            + XFormsConstants.XFORMS_NAMESPACE_URI + "']/*[1]";

    // External resources
    private final static String REVERSE_SETVALUES_XSL = "oxf:/ops/pfc/setvalue-reverse.xsl";
    private final static String REWRITE_XSL = "oxf:/ops/pfc/rewrite.xsl";
    private final static String XFORMS_XML_SUBMISSION_XPL = "oxf:/ops/pfc/xforms-xml-submission.xpl";

    // Instance passing configuration
    private final static String INSTANCE_PASSING_REDIRECT = "redirect";
    private final static String INSTANCE_PASSING_FORWARD = "forward";
    private final static String INSTANCE_PASSING_REDIRECT_PORTAL = "redirect-exit-portal";
    private final static String DEFAULT_INSTANCE_PASSING = INSTANCE_PASSING_REDIRECT;

    // Attributes
    private final static String VERSIONED_ATTRIBUTE = "versioned";

    // Properties
    private static final String INSTANCE_PASSING_PROPERTY_NAME = "instance-passing";
    private static final String EPILOGUE_PROPERTY_NAME = "epilogue";
    private static final String NOT_FOUND_PROPERTY_NAME = "not-found";
    private static final String XFORMS_SUBMISSION_MODEL_PROPERTY_NAME = "xforms-submission-model";

    public static final String XFORMS_SUBMISSION_PATH_PROPERTY_NAME = "xforms-submission-path";
    public static final String XFORMS_SUBMISSION_PATH_DEFAULT_VALUE = "/xforms-server-submit";
    public static final String PATH_MATCHERS = "path-matchers"; // used by PageFlowController

    static {
        Element trueConfigElement = new NonLazyUserDataElement("config");
        trueConfigElement.setText("true");
        TRUE_DOCUMENT.setRootElement(trueConfigElement);

        Element falseConfigElement = new NonLazyUserDataElement("config");
        falseConfigElement.setText("false");
        FALSE_DOCUMENT.setRootElement(falseConfigElement);

        final Map<String, String> mapping = new HashMap<String, String>();
        mapping.put(XMLConstants.XSI_PREFIX, XMLConstants.XSI_URI);
        mapping.put(XMLConstants.XSLT_PREFIX, XMLConstants.XSLT_NAMESPACE_URI);

        NAMESPACES_WITH_XSI_AND_XSLT = new NamespaceMapping(mapping);
    }

    public PageFlowControllerProcessor() {
        addInputInfo(new ProcessorInputOutputInfo(INPUT_CONTROLLER, CONTROLLER_NAMESPACE_URI));
    }

    public void start(PipelineContext pipelineContext) {
        final PageFlow pageFlow = (PageFlow) readCacheInputAsObject(pipelineContext,
                getInputByName(INPUT_CONTROLLER), new CacheableInputReader() {
                    public Object read(final PipelineContext context, ProcessorInput input) {
                        final Document controllerDocument = readInputAsDOM4J(context, INPUT_CONTROLLER);
                        final Object controllerValidity = getInputValidity(context,
                                getInputByName(INPUT_CONTROLLER));
                        final StepProcessorContext stepProcessorContext = new StepProcessorContext(
                                controllerValidity);
                        LocationData locationData = (LocationData) controllerDocument.getRootElement().getData();
                        final String controllerContext = locationData == null ? null : locationData.getSystemID();

                        // Get global "parameters" of the controller, fall back to properties
                        final String globalInstancePassing;
                        {
                            final String attributeValue = controllerDocument.getRootElement()
                                    .attributeValue(INSTANCE_PASSING_PROPERTY_NAME);
                            globalInstancePassing = attributeValue != null ? attributeValue
                                    : getPropertySet().getString(INSTANCE_PASSING_PROPERTY_NAME,
                                            DEFAULT_INSTANCE_PASSING);
                        }
                        final boolean globalIsVersioned;
                        {
                            final String attributeValue = controllerDocument.getRootElement()
                                    .attributeValue(VERSIONED_ATTRIBUTE);
                            // NOTE: We use a global property, not an oxf:page-flow scoped one
                            globalIsVersioned = attributeValue != null ? Boolean.valueOf(attributeValue)
                                    : URLRewriterUtils.isResourcesVersioned();
                        }
                        final String epilogueURL;
                        final Element epilogueElement;
                        {
                            epilogueElement = controllerDocument.getRootElement().element("epilogue");
                            epilogueURL = epilogueElement != null ? epilogueElement.attributeValue("url")
                                    : getPropertySet().getString(EPILOGUE_PROPERTY_NAME);
                        }
                        final String notFoundPipeline = getPropertySet().getString(NOT_FOUND_PROPERTY_NAME);
                        final String notFoundPageId;
                        {
                            final Element notFoundHandlerElement = controllerDocument.getRootElement()
                                    .element("not-found-handler");
                            notFoundPageId = notFoundHandlerElement != null
                                    ? notFoundHandlerElement.attributeValue("page")
                                    : null;
                        }
                        //                final String errorPageId; {
                        //                    Element errorHandlerElement = controllerDocument.getRootElement().element("error-handler");
                        //                    errorPageId = errorHandlerElement != null ? errorHandlerElement.attributeValue("page") : null;
                        //                }

                        final List<URLRewriterUtils.PathMatcher> pathMatchers = new ArrayList<URLRewriterUtils.PathMatcher>();

                        // XForms Submission page
                        {
                            final String xformsSubmissionPath = getPropertySet().getString(
                                    XFORMS_SUBMISSION_PATH_PROPERTY_NAME, XFORMS_SUBMISSION_PATH_DEFAULT_VALUE);
                            final String xformsSubmissionModel = getPropertySet()
                                    .getStringOrURIAsString(XFORMS_SUBMISSION_MODEL_PROPERTY_NAME);
                            if ((xformsSubmissionPath == null && xformsSubmissionModel != null)
                                    || (xformsSubmissionPath != null && xformsSubmissionModel == null)) {
                                throw new OXFException(
                                        "Only one of properties " + XFORMS_SUBMISSION_PATH_PROPERTY_NAME + " and "
                                                + XFORMS_SUBMISSION_MODEL_PROPERTY_NAME + " is set.");
                            }
                            if (xformsSubmissionPath != null) {
                                final Element firstPageElement = controllerDocument.getRootElement()
                                        .element("page");
                                if (firstPageElement != null) {
                                    final List<Element> allElements = Dom4jUtils
                                            .elements(controllerDocument.getRootElement());
                                    final int firstPageElementIndex = allElements.indexOf(firstPageElement);
                                    final Element newElement = Dom4jUtils.createElement("page",
                                            CONTROLLER_NAMESPACE_URI);
                                    newElement.addAttribute("path-info", xformsSubmissionPath);
                                    newElement.addAttribute("model", xformsSubmissionModel);
                                    allElements.add(firstPageElementIndex, newElement);
                                }
                            }
                        }

                        // Go through all pages to get mapping
                        final Map<String, Element> pageIdToPageElement = new HashMap<String, Element>();
                        final Map<String, String> pageIdToPathInfo = new HashMap<String, String>();
                        final Map<String, String> pageIdToXFormsModel = new HashMap<String, String>();
                        final Map<String, Document> pageIdToSetvaluesDocument = new HashMap<String, Document>();
                        final int pageCount = controllerDocument.getRootElement().elements("page").size();

                        for (Object o : controllerDocument.getRootElement().elements("page")) {
                            Element pageElement = (Element) o;
                            String pathInfo = pageElement.attributeValue("path-info");
                            String xformsModel = pageElement.attributeValue("xforms");
                            String id = pageElement.attributeValue("id");
                            if (id != null) {
                                pageIdToPageElement.put(id, pageElement);
                                pageIdToPathInfo.put(id, pathInfo);
                                if (xformsModel != null)
                                    pageIdToXFormsModel.put(id, xformsModel);

                                final Document setvaluesDocument = getSetValuesDocument(pageElement);
                                if (setvaluesDocument != null) {
                                    pageIdToSetvaluesDocument.put(id, setvaluesDocument);
                                }
                            }
                        }

                        ASTPipeline astPipeline = new ASTPipeline() {
                            {

                                setValidity(controllerValidity);

                                // Generate request path
                                final ASTOutput request = new ASTOutput("data", "request");
                                addStatement(new ASTProcessorCall(XMLConstants.REQUEST_PROCESSOR_QNAME) {
                                    {
                                        final Document config;
                                        try {
                                            config = Dom4jUtils.readDom4j(
                                                    "<config><include>/request/request-path</include><include>/request/method</include></config>");
                                        } catch (Exception e) {
                                            throw new OXFException(e);
                                        }
                                        addInput(new ASTInput("config", config));
                                        addOutput(request);
                                    }
                                });

                                // Dummy matcher output
                                final ASTOutput dummyMatcherOutput = new ASTOutput("data", "dummy-matcher");
                                addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                    {
                                        addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                        addOutput(dummyMatcherOutput);
                                    }
                                });

                                // Is the previous <files> or <page> using simple matching
                                boolean previousIsSimple = true;
                                boolean previousIsFile = false;
                                boolean isFirst = true;
                                ASTChoose currentChoose = null;
                                int matcherCount = 0;

                                final ASTOutput epilogueData = new ASTOutput(null, "html");
                                final ASTOutput epilogueModelData = new ASTOutput(null, "epilogue-model-data");
                                final ASTOutput epilogueInstance = new ASTOutput(null, "epilogue-instance");
                                for (Object o : controllerDocument.getRootElement().elements()) {
                                    Element element = (Element) o;
                                    if ("files".equals(element.getName()) || "page".equals(element.getName())) {

                                        // Extract matcher URI or QName
                                        // URI is supported for backward compatibility only. We use a poor
                                        // heuristic to detect whether a QName is present.
                                        final String matcherURI;
                                        final QName matcherQName;
                                        {
                                            final String matcherAttribute = element.attributeValue("matcher");
                                            if (matcherAttribute != null && matcherAttribute.indexOf(':') != -1) {
                                                matcherQName = Dom4jUtils.extractAttributeValueQName(element,
                                                        "matcher");
                                                matcherURI = null;
                                            } else {
                                                matcherQName = null;
                                                matcherURI = matcherAttribute;
                                            }
                                        }

                                        final String mimeType = element.attributeValue("mime-type");
                                        final String pathInfo = element.attributeValue("path-info");

                                        final boolean currentIsFile = "files".equals(element.getName());
                                        final boolean currentFileIsVersioned;
                                        if (currentIsFile) {
                                            // If this is a file, then check "versioned" property, local and then global
                                            final String currentIsVersionedAttribute = element
                                                    .attributeValue(VERSIONED_ATTRIBUTE);
                                            currentFileIsVersioned = (currentIsVersionedAttribute != null)
                                                    ? "true".equals(currentIsVersionedAttribute)
                                                    : globalIsVersioned;
                                        } else {
                                            currentFileIsVersioned = false;
                                        }

                                        // Remember this FilesInfo if needed
                                        if (currentFileIsVersioned) {
                                            pathMatchers.add(new URLRewriterUtils.PathMatcher(pathInfo,
                                                    matcherQName, mimeType, true));
                                        }

                                        // Can we just add this condition to the previous "when" statement?
                                        boolean canAddToPreviousWhen = previousIsFile && currentIsFile
                                                && previousIsSimple && matcherURI == null && matcherQName == null
                                                && mimeType == null;

                                        // Create <p:when>
                                        ASTWhen when;
                                        final ASTOutput matcherOutput;
                                        if (matcherURI == null && matcherQName == null) {
                                            // There is no matcher, use basic rules

                                            matcherOutput = dummyMatcherOutput;

                                            // Add <p:choose> / <p:otherwise> when necessary
                                            if (!previousIsSimple) {
                                                // Add a <p:otherwise><p:choose ref="request">
                                                previousIsSimple = mimeType == null;
                                                ASTWhen otherwise = new ASTWhen();
                                                currentChoose.getWhen().add(otherwise);
                                                currentChoose = new ASTChoose(new ASTHrefId(request));
                                                otherwise.getStatements().add(currentChoose);
                                            } else if (isFirst) {
                                                // Add a <p:choose ref="request">
                                                isFirst = false;
                                                currentChoose = new ASTChoose(new ASTHrefId(request));
                                                addStatement(currentChoose);
                                            }

                                            // Compute test
                                            final String test;
                                            if (pathInfo.startsWith("*")) {
                                                // Extension match
                                                test = "ends-with(/request/request-path, '" + pathInfo.substring(1)
                                                        + "' )";
                                            } else if (pathInfo.endsWith("*")) {
                                                // Partial match
                                                final int len = pathInfo.length() - 1;
                                                test = "starts-with(/request/request-path, '"
                                                        + pathInfo.substring(0, len) + "' )";
                                            } else {
                                                // Exact match
                                                test = "(/request/request-path = '" + pathInfo + "')";
                                            }

                                            // Add <p:when>
                                            when = new ASTWhen(test);
                                        } else {
                                            // Use matcher

                                            previousIsSimple = false;

                                            // List if statements where we add the new <p:choose>
                                            List<ASTStatement> statements;
                                            if (isFirst) {
                                                isFirst = false;
                                                statements = getStatements();
                                            } else {
                                                ASTWhen otherwise = new ASTWhen();
                                                currentChoose.getWhen().add(otherwise);
                                                statements = otherwise.getStatements();
                                            }

                                            // Execute regexp
                                            final ASTOutput realMatcherOutput = new ASTOutput("data",
                                                    "matcher-" + (++matcherCount));
                                            matcherOutput = realMatcherOutput;
                                            statements.add(new ASTProcessorCall(matcherQName, matcherURI) {
                                                {
                                                    Document config = new NonLazyUserDataDocument(
                                                            new NonLazyUserDataElement("regexp"));
                                                    config.getRootElement().addText(pathInfo);
                                                    addInput(new ASTInput("config", config));
                                                    addInput(new ASTInput("data", new ASTHrefXPointer(
                                                            new ASTHrefId(request), "/request/request-path")));
                                                    addOutput(realMatcherOutput);
                                                }
                                            });

                                            // Add <p:choose>
                                            currentChoose = new ASTChoose(new ASTHrefId(matcherOutput));
                                            statements.add(currentChoose);
                                            when = new ASTWhen("/result/matches = 'true'");
                                        }

                                        if (canAddToPreviousWhen) {
                                            // Do not create new "when", add current condition to previous "when"
                                            final ASTWhen previousWhen = currentChoose.getWhen()
                                                    .get(currentChoose.getWhen().size() - 1);
                                            previousWhen.setTest(previousWhen.getTest() + " or " + when.getTest());
                                        } else {
                                            // Create new "when"
                                            currentChoose.getWhen().add(when);
                                            if (currentIsFile) {
                                                // Handle file
                                                previousIsFile = true;

                                                // For files, enforce GET method
                                                if (matcherURI == null && matcherQName == null)// TODO: should do this when we have matchers as well, but the condition applies to the #matcher-* input
                                                    when.setTest(
                                                            "/request/method = 'GET' and (" + when.getTest() + ")");

                                                handleFile(when, request, mimeType, epilogueData, epilogueModelData,
                                                        epilogueInstance);
                                            } else if ("page".equals(element.getName())) {
                                                // Handle page
                                                previousIsFile = false;
                                                // Get unique page number
                                                int pageNumber = element.getParent().elements().indexOf(element);
                                                handlePage(stepProcessorContext, controllerContext,
                                                        when.getStatements(), element, pageNumber, matcherOutput,
                                                        epilogueData, epilogueModelData, epilogueInstance,
                                                        pageIdToPathInfo, pageIdToXFormsModel,
                                                        pageIdToSetvaluesDocument, globalInstancePassing);
                                            }
                                        }
                                    }
                                }

                                // Create "not found" page
                                if (notFoundPipeline != null || notFoundPageId != null) {

                                    // Determine where we insert out statements
                                    List<ASTStatement> statementsList;
                                    if (currentChoose == null) {
                                        statementsList = getStatements();
                                    } else {
                                        ASTWhen otherwise = new ASTWhen();
                                        currentChoose.getWhen().add(otherwise);
                                        statementsList = otherwise.getStatements();
                                    }

                                    if (notFoundPageId != null) {
                                        // Handle not-found page
                                        // FIXME: We do not support not-found pages with a matcher output for now.
                                        Element notFoundPageElement = pageIdToPageElement.get(notFoundPageId);
                                        if (notFoundPageElement == null)
                                            throw new OXFException("Cannot find \"not found\" page with id '"
                                                    + notFoundPageId + "' in page flow");
                                        // Create an artificial page number (must be different from the other page numbers)
                                        handlePage(stepProcessorContext, controllerContext, statementsList,
                                                notFoundPageElement, pageCount, dummyMatcherOutput, epilogueData,
                                                epilogueModelData, epilogueInstance, pageIdToPathInfo,
                                                pageIdToXFormsModel, pageIdToSetvaluesDocument,
                                                globalInstancePassing);
                                    } else {
                                        // [BACKWARD COMPATIBILITY] - Execute simple "not-found" page coming from properties
                                        final ASTOutput notFoundHTML = new ASTOutput(null, "not-found-html");
                                        statementsList.add(new StepProcessorCall(stepProcessorContext,
                                                controllerContext, notFoundPipeline, "not-found") {
                                            {
                                                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                                addInput(new ASTInput("instance", Dom4jUtils.NULL_DOCUMENT));
                                                addInput(new ASTInput("xforms-model", Dom4jUtils.NULL_DOCUMENT));
                                                addInput(new ASTInput("matcher", Dom4jUtils.NULL_DOCUMENT));
                                                final ASTOutput dataOutput = new ASTOutput("data", notFoundHTML);
                                                dataOutput.setLocationData(new ExtendedLocationData(
                                                        (LocationData) controllerDocument.getRootElement()
                                                                .getData(),
                                                        "executing not found pipeline",
                                                        new String[] { "pipeline", notFoundPipeline }, true)); // use root element location data
                                                addOutput(dataOutput);
                                            }
                                        });

                                        // There is no model data to send to the epilogue
                                        statementsList
                                                .add(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                                    {
                                                        addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                                        addOutput(new ASTOutput("data", epilogueModelData));
                                                    }
                                                });

                                        // Send not-found through epilogue
                                        handleEpilogue(controllerContext, statementsList, epilogueURL,
                                                epilogueElement, notFoundHTML, epilogueModelData, epilogueInstance,
                                                404);

                                        // Notify final epilogue that there is nothing to send
                                        statementsList
                                                .add(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                                    {
                                                        addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                                        addOutput(new ASTOutput("data", epilogueData));
                                                    }
                                                });
                                        statementsList
                                                .add(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                                    {
                                                        addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                                        addOutput(new ASTOutput("data", epilogueInstance));
                                                    }
                                                });
                                    }
                                }

                                // Handle view, if there was one
                                addStatement(new ASTChoose(new ASTHrefId(epilogueData)) {
                                    {
                                        addWhen(new ASTWhen("not(/*/@xsi:nil = 'true')") {
                                            {
                                                setNamespaces(NAMESPACES_WITH_XSI_AND_XSLT);
                                                handleEpilogue(controllerContext, getStatements(), epilogueURL,
                                                        epilogueElement, epilogueData, epilogueModelData,
                                                        epilogueInstance, 200);
                                            }
                                        });
                                        addWhen(new ASTWhen() {
                                            {
                                                // Make sure we execute the model if there is a model but no view
                                                addStatement(new ASTProcessorCall(
                                                        XMLConstants.NULL_SERIALIZER_PROCESSOR_QNAME) {
                                                    {
                                                        addInput(new ASTInput("data",
                                                                new ASTHrefId(epilogueModelData)));
                                                    }
                                                });
                                            }
                                        });
                                    }
                                });
                            }
                        };

                        // For debugging
                        if (logger.isDebugEnabled()) {
                            ASTDocumentHandler astDocumentHandler = new ASTDocumentHandler();
                            astPipeline.walk(astDocumentHandler);
                            logger.debug("Page Flow Controller pipeline:\n"
                                    + Dom4jUtils.domToString(astDocumentHandler.getDocument()));
                        }

                        return new PageFlow(new PipelineProcessor(astPipeline),
                                Collections.unmodifiableList(pathMatchers));
                    }
                });

        // If required, store information about resources to rewrite in the pipeline context for downstream use, e.g. by
        // oxf:xhtml-rewrite. This allows consumers who would like to rewrite resources into versioned resources to
        // actually know what a "resource" is.
        final List<URLRewriterUtils.PathMatcher> pathMatchers = pageFlow.getPathMatchers();
        if (pathMatchers != null && pathMatchers.size() > 0) {
            final List<URLRewriterUtils.PathMatcher> existingPathMatchers = (List<URLRewriterUtils.PathMatcher>) pipelineContext
                    .getAttribute(PATH_MATCHERS);
            if (existingPathMatchers == null) {
                // Set if we are the first
                pipelineContext.setAttribute(PATH_MATCHERS, pathMatchers);
            } else {
                // Add if we come after others (in case of nested page flows)
                final List<URLRewriterUtils.PathMatcher> allMatchers = new ArrayList<URLRewriterUtils.PathMatcher>() {
                    {
                        addAll(existingPathMatchers);
                        addAll(pathMatchers);
                    }
                };
                pipelineContext.setAttribute(PATH_MATCHERS, Collections.unmodifiableList(allMatchers));
            }
        }

        // Launch pipeline
        final PipelineProcessor pipelineProcessor = pageFlow.getPipelineProcessor();
        pipelineProcessor.reset(pipelineContext);
        pipelineProcessor.start(pipelineContext);
    }

    private static void handleEpilogue(final String controllerContext, List<ASTStatement> statements,
            final String epilogueURL, final Element epilogueElement, final ASTOutput epilogueData,
            final ASTOutput epilogueModelData, final ASTOutput epilogueInstance, final int defaultStatusCode) {
        // Send result through epilogue
        if (epilogueURL == null) {
            // Run HTML serializer
            statements.add(new ASTChoose(new ASTHrefId(epilogueData)) {
                {
                    addWhen(new ASTWhen("not(/*/@xsi:nil = 'true')") {
                        {
                            setNamespaces(NAMESPACES_WITH_XSI_AND_XSLT);
                            // The epilogue did not do the serialization
                            addStatement(new ASTProcessorCall(XMLConstants.HTML_SERIALIZER_PROCESSOR_QNAME) {
                                {
                                    Document config = new NonLazyUserDataDocument(
                                            new NonLazyUserDataElement("config"));
                                    Element rootElement = config.getRootElement();
                                    rootElement.addElement("status-code")
                                            .addText(Integer.toString(defaultStatusCode));
                                    if (HTMLSerializer.DEFAULT_PUBLIC_DOCTYPE != null)
                                        rootElement.addElement("public-doctype")
                                                .addText(HTMLSerializer.DEFAULT_PUBLIC_DOCTYPE);
                                    if (HTMLSerializer.DEFAULT_SYSTEM_DOCTYPE != null)
                                        rootElement.addElement("system-doctype")
                                                .addText(HTMLSerializer.DEFAULT_SYSTEM_DOCTYPE);
                                    if (HTMLSerializer.DEFAULT_VERSION != null)
                                        rootElement.addElement("version").addText(HTMLSerializer.DEFAULT_VERSION);
                                    addInput(new ASTInput("config", config));
                                    addInput(new ASTInput("data", new ASTHrefId(epilogueData)));
                                    //setLocationData(TODO);
                                }
                            });
                        }
                    });
                    addWhen(new ASTWhen());
                }
            });
        } else {
            statements.add(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {
                {
                    final String url;
                    try {
                        url = URLFactory.createURL(controllerContext, epilogueURL).toExternalForm();
                    } catch (MalformedURLException e) {
                        throw new OXFException(e);
                    }
                    addInput(new ASTInput("config", new ASTHrefURL(url)));
                    addInput(new ASTInput("data", new ASTHrefId(epilogueData)));
                    addInput(new ASTInput("model-data", new ASTHrefId(epilogueModelData)));
                    addInput(new ASTInput("instance", new ASTHrefId(epilogueInstance)));
                    final String[] locationParams = new String[] { "pipeline", epilogueURL };
                    setLocationData(new ExtendedLocationData((LocationData) epilogueElement.getData(),
                            "executing epilogue", epilogueElement, locationParams, true));
                }
            });
        }
    }

    private Document getSetValuesDocument(final Element pageElement) {
        final List paramElements = pageElement.elements("param");
        final List setValueElements = pageElement.elements("setvalue");
        final Document setvaluesDocument;
        if (!paramElements.isEmpty() || !setValueElements.isEmpty()) {
            // Create document with setvalues
            setvaluesDocument = new NonLazyUserDataDocument(new NonLazyUserDataElement("params"));
            // Deprecated <param> elements
            if (!paramElements.isEmpty()) {
                for (Object paramElement1 : paramElements) {
                    final Element paramElement = (Element) paramElement1;
                    setvaluesDocument.getRootElement().add((Element) paramElement.clone());
                }
            }
            // New <setvalue> elements
            if (!setValueElements.isEmpty()) {
                for (Object setValueElement1 : setValueElements) {
                    final Element setValueElement = (Element) setValueElement1;
                    setvaluesDocument.getRootElement().add((Element) setValueElement.clone());
                }
            }
        } else {
            setvaluesDocument = null;
        }
        return setvaluesDocument;
    }

    /**
     * Handle <page>
     */
    private void handlePage(final StepProcessorContext stepProcessorContext, final String controllerContext,
            List<ASTStatement> statementsList, final Element pageElement, final int pageNumber,
            final ASTOutput matcherOutput, final ASTOutput viewData, final ASTOutput epilogueModelData,
            final ASTOutput viewInstance, final Map<String, String> pageIdToPathInfo,
            final Map<String, String> pageIdToXFormsModel, final Map<String, Document> pageIdToSetvaluesDocument,
            final String instancePassing) {

        // Get page attributes
        final String modelAttribute = pageElement.attributeValue("model");
        final String viewAttribute = pageElement.attributeValue("view");
        final String defaultSubmissionAttribute = pageElement.attributeValue("default-submission");

        // Get setvalues document
        final Document setvaluesDocument = getSetValuesDocument(pageElement);

        // Get actions
        final List actionElements = pageElement.elements("action");

        // Handle initial instance
        final ASTOutput defaultSubmission = new ASTOutput("data", "default-submission");
        if (defaultSubmissionAttribute != null) {
            statementsList.add(new ASTProcessorCall(XMLConstants.URL_GENERATOR_PROCESSOR_QNAME) {
                {
                    final String url;
                    try {
                        url = URLFactory.createURL(controllerContext, defaultSubmissionAttribute).toExternalForm();
                    } catch (MalformedURLException e) {
                        throw new OXFException(e);
                    }
                    final Document configDocument = new NonLazyUserDataDocument(
                            new NonLazyUserDataElement("config"));
                    configDocument.getRootElement().addText(url);

                    addInput(new ASTInput("config", configDocument));
                    addOutput(defaultSubmission);
                }
            });
        }

        // XForms Input
        final ASTOutput isRedirect = new ASTOutput(null, "is-redirect");

        // Always hook up XForms or XML submission
        final ASTOutput xformedInstance = new ASTOutput("instance", "xformed-instance");
        {
            final LocationData locDat = Dom4jUtils.getLocationData();
            xformedInstance.setLocationData(locDat);
        }

        // Use XML Submission pipeline
        statementsList.add(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {
            {
                addInput(new ASTInput("config", new ASTHrefURL(XFORMS_XML_SUBMISSION_XPL)));
                if (setvaluesDocument != null) {
                    addInput(new ASTInput("setvalues", setvaluesDocument));
                    addInput(new ASTInput("matcher-result", new ASTHrefId(matcherOutput)));
                } else {
                    addInput(new ASTInput("setvalues", Dom4jUtils.NULL_DOCUMENT));
                    addInput(new ASTInput("matcher-result", Dom4jUtils.NULL_DOCUMENT));
                }
                if (defaultSubmissionAttribute != null) {
                    addInput(new ASTInput("default-submission", new ASTHrefId(defaultSubmission)));
                } else {
                    addInput(new ASTInput("default-submission", Dom4jUtils.NULL_DOCUMENT));
                }
                addOutput(xformedInstance);
            }
        });

        // Make sure the xformed-instance id is used for p:choose
        statementsList.add(new ASTProcessorCall(XMLConstants.NULL_PROCESSOR_QNAME) {
            {
                addInput(new ASTInput("data", new ASTHrefId(xformedInstance)));
            }
        });

        // Execute actions
        final ASTOutput xupdatedInstance = new ASTOutput(null, "xupdated-instance");
        final ASTOutput actionData = new ASTOutput(null, "action-data");
        final int[] actionNumber = new int[] { 0 };
        final boolean[] foundActionWithoutWhen = new boolean[] { false };
        final ASTChoose actionsChoose = new ASTChoose(new ASTHrefId(xformedInstance)) {
            {

                // Always add a branch to test on whether the XML submission asked to bypass actions, model, view, and epilogue
                // Use of this <bypass> document is arguably a HACK 
                addWhen(new ASTWhen() {
                    {

                        setTest("/bypass[@xsi:nil = 'true']");
                        setNamespaces(NAMESPACES_WITH_XSI_AND_XSLT);

                        addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                addOutput(new ASTOutput("data", xupdatedInstance));
                            }
                        });
                        addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                            {
                                final Document config = new NonLazyUserDataDocument(
                                        new NonLazyUserDataElement("is-redirect"));
                                config.getRootElement().addText("true");
                                addInput(new ASTInput("data", config));
                                addOutput(new ASTOutput("data", isRedirect));
                            }
                        });
                        addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                addOutput(new ASTOutput("data", actionData));
                            }
                        });
                    }
                });

                for (Object actionElement1 : actionElements) {

                    // Get info about action
                    actionNumber[0]++;
                    final Element actionElement = (Element) actionElement1;
                    final String whenAttribute = actionElement.attributeValue("when");
                    final String actionAttribute = actionElement.attributeValue("action");

                    // Execute action
                    addWhen(new ASTWhen() {
                        {

                            // Add condition, remember that we found an <action> without a when
                            if (whenAttribute != null) {
                                if (foundActionWithoutWhen[0])
                                    throw new ValidationException("Unreachable <action>",
                                            (LocationData) actionElement.getData());
                                setTest(whenAttribute);
                                setNamespaces(new NamespaceMapping(
                                        Dom4jUtils.getNamespaceContextNoDefault(actionElement)));
                                setLocationData((LocationData) actionElement.getData());
                            } else {
                                foundActionWithoutWhen[0] = true;
                            }

                            final boolean resultTestsOnActionData =
                                    // Must have an action, in the first place
                                    actionAttribute != null &&
                            // More than one <result>: so at least the first one must have a "when"
                            actionElement.elements("result").size() > 1;

                            final ASTOutput internalActionData = actionAttribute == null ? null
                                    : new ASTOutput(null,
                                            "internal-action-data-" + pageNumber + "-" + actionNumber[0]);
                            if (actionAttribute != null) {
                                // TODO: handle passing and modifications of action data in model, and view, and pass to instance
                                addStatement(new StepProcessorCall(stepProcessorContext, controllerContext,
                                        actionAttribute, "action") {
                                    {
                                        addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                        addInput(new ASTInput("instance", new ASTHrefId(xformedInstance)));
                                        addInput(new ASTInput("xforms-model", Dom4jUtils.NULL_DOCUMENT));
                                        addInput(new ASTInput("matcher", new ASTHrefId(matcherOutput)));
                                        final ASTOutput dataOutput = new ASTOutput("data", internalActionData);
                                        final String[] locationParams = new String[] { "pipeline", actionAttribute,
                                                "page id", pageElement.attributeValue("id"), "when",
                                                whenAttribute };
                                        dataOutput.setLocationData(new ExtendedLocationData(
                                                (LocationData) actionElement.getData(),
                                                "reading action data output", pageElement, locationParams, true));
                                        addOutput(dataOutput);
                                        setLocationData(
                                                new ExtendedLocationData((LocationData) actionElement.getData(),
                                                        "executing action", pageElement, locationParams, true));
                                    }
                                });

                                // Force execution of action if no <result> is reading it
                                if (!resultTestsOnActionData) {
                                    addStatement(
                                            new ASTProcessorCall(XMLConstants.NULL_SERIALIZER_PROCESSOR_QNAME) {
                                                {
                                                    addInput(new ASTInput("data",
                                                            new ASTHrefId(internalActionData)));
                                                }
                                            });
                                }

                                // Export internal-action-data as action-data
                                addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                    {
                                        addInput(new ASTInput("data", new ASTHrefId(internalActionData)));
                                        addOutput(new ASTOutput("data", actionData));
                                    }
                                });
                            } else {
                                addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                    {
                                        addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                        addOutput(new ASTOutput("data", actionData));
                                    }
                                });
                            }

                            // Choose result
                            if (actionElement.elements("result").size() > 1
                                    || (actionElement.element("result") != null
                                            && actionElement.element("result").attributeValue("when") != null)) {

                                // Help diagnose missing action/@action
                                if (internalActionData == null) {
                                    throw new OXFException(
                                            "Found <result when=\"...\"> but <action> element is missing an action attribute.");
                                }

                                // Test based on action
                                addStatement(new ASTChoose(new ASTHrefId(internalActionData)) {
                                    {
                                        for (Object o : actionElement.elements("result")) {
                                            final Element resultElement = (Element) o;
                                            final String resultWhenAttribute = resultElement.attributeValue("when");

                                            // Execute result
                                            addWhen(new ASTWhen() {
                                                {
                                                    if (resultWhenAttribute != null) {
                                                        setTest(resultWhenAttribute);
                                                        setNamespaces(new NamespaceMapping(Dom4jUtils
                                                                .getNamespaceContextNoDefault(resultElement)));
                                                        final String[] locationParams = new String[] { "page id",
                                                                pageElement.attributeValue("id"), "when",
                                                                resultWhenAttribute };
                                                        setLocationData(new ExtendedLocationData(
                                                                (LocationData) resultElement.getData(),
                                                                "executing result", resultElement, locationParams,
                                                                true));
                                                    }
                                                    executeResult(stepProcessorContext, controllerContext, this,
                                                            pageIdToXFormsModel, pageIdToPathInfo,
                                                            pageIdToSetvaluesDocument, xformedInstance,
                                                            resultElement, internalActionData, isRedirect,
                                                            xupdatedInstance, instancePassing);
                                                }
                                            });
                                        }

                                        // Continue when all results fail
                                        addWhen(new ASTWhen() {
                                            {
                                                addStatement(new ASTProcessorCall(
                                                        XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                                    {
                                                        addInput(new ASTInput("data", new NonLazyUserDataDocument(
                                                                new NonLazyUserDataElement("is-redirect") {
                                                                    {
                                                                        setText("false");
                                                                    }
                                                                })));
                                                        addOutput(new ASTOutput("data", isRedirect));
                                                    }
                                                });
                                                addStatement(new ASTProcessorCall(
                                                        XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                                    {
                                                        addInput(new ASTInput("data",
                                                                new ASTHrefId(xformedInstance)));
                                                        addOutput(new ASTOutput("data", xupdatedInstance));
                                                    }
                                                });
                                            }
                                        });
                                    }
                                });

                            } else {

                                // If we are not performing tests on the result from the action
                                final Element resultElement = actionElement.element("result");
                                executeResult(stepProcessorContext, controllerContext, this, pageIdToXFormsModel,
                                        pageIdToPathInfo, pageIdToSetvaluesDocument, xformedInstance, resultElement,
                                        internalActionData, isRedirect, xupdatedInstance, instancePassing);
                            }
                        }
                    });
                }

                if (!foundActionWithoutWhen[0]) {
                    // Defaul branch for when all actions fail
                    addWhen(new ASTWhen() {
                        {
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(xformedInstance)));
                                    addOutput(new ASTOutput("data", xupdatedInstance));
                                }
                            });
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    final Document config = new NonLazyUserDataDocument(
                                            new NonLazyUserDataElement("is-redirect"));
                                    config.getRootElement().addText("false");
                                    addInput(new ASTInput("data", config));
                                    addOutput(new ASTOutput("data", isRedirect));
                                }
                            });
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                    addOutput(new ASTOutput("data", actionData));
                                }
                            });
                        }
                    });
                }
            }
        };

        // Add choose statement
        statementsList.add(actionsChoose);

        // Only continue if there was no redirect
        statementsList.add(new ASTChoose(new ASTHrefId(isRedirect)) {
            {
                addWhen(new ASTWhen("/is-redirect = 'false'") {
                    {

                        // Handle page model
                        final ASTOutput modelData = new ASTOutput(null, "model-data");
                        final ASTOutput modelInstance = new ASTOutput(null, "model-instance");
                        if (modelAttribute != null) {
                            // There is a model
                            addStatement(new StepProcessorCall(stepProcessorContext, controllerContext,
                                    modelAttribute, "model") {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(actionData)));
                                    addInput(new ASTInput("instance", new ASTHrefId(xupdatedInstance)));
                                    addInput(new ASTInput("xforms-model", Dom4jUtils.NULL_DOCUMENT));
                                    addInput(new ASTInput("matcher", new ASTHrefId(matcherOutput)));
                                    final String[] locationParams = new String[] { "page id",
                                            pageElement.attributeValue("id"), "model", modelAttribute };
                                    {
                                        final ASTOutput dataOutput = new ASTOutput("data", modelData);
                                        dataOutput.setLocationData(
                                                new ExtendedLocationData((LocationData) pageElement.getData(),
                                                        "reading page model data output", pageElement,
                                                        locationParams, true));
                                        addOutput(dataOutput);
                                    }
                                    {
                                        final ASTOutput instanceOutput = new ASTOutput("instance", modelInstance);
                                        addOutput(instanceOutput);
                                        instanceOutput.setLocationData(
                                                new ExtendedLocationData((LocationData) pageElement.getData(),
                                                        "reading page model instance output", pageElement,
                                                        locationParams, true));
                                    }
                                    setLocationData(new ExtendedLocationData((LocationData) pageElement.getData(),
                                            "executing page model", pageElement, locationParams, true));
                                }
                            });
                        } else if (viewAttribute != null) {
                            // There is no model but there is a view
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(actionData)));
                                    addOutput(new ASTOutput("data", modelData));
                                }
                            });
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(xupdatedInstance)));
                                    addOutput(new ASTOutput("data", modelInstance));
                                }
                            });
                        }

                        // Handle page view
                        if (viewAttribute != null) {
                            // There is a view
                            addStatement(new StepProcessorCall(stepProcessorContext, controllerContext,
                                    viewAttribute, "view") {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(modelData)));
                                    addInput(new ASTInput("instance", new ASTHrefId(modelInstance)));
                                    addInput(new ASTInput("xforms-model", Dom4jUtils.NULL_DOCUMENT));
                                    addInput(new ASTInput("matcher", new ASTHrefId(matcherOutput)));
                                    final String[] locationParams = new String[] { "page id",
                                            pageElement.attributeValue("id"), "view", viewAttribute };
                                    {
                                        final ASTOutput dataOutput = new ASTOutput("data", viewData);
                                        dataOutput.setLocationData(
                                                new ExtendedLocationData((LocationData) pageElement.getData(),
                                                        "reading page view data output", pageElement,
                                                        locationParams, true));
                                        addOutput(dataOutput);
                                    }
                                    {
                                        final ASTOutput instanceOutput = new ASTOutput("instance", viewInstance);
                                        instanceOutput.setLocationData(
                                                new ExtendedLocationData((LocationData) pageElement.getData(),
                                                        "reading page view instance output", pageElement,
                                                        locationParams, true));
                                        addOutput(instanceOutput);
                                    }
                                    setLocationData(new ExtendedLocationData((LocationData) pageElement.getData(),
                                            "executing page view", pageElement, locationParams, true));
                                }
                            });
                        } else {
                            // There is no view, send nothing to epilogue
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                    addOutput(new ASTOutput("data", viewData));
                                }
                            });
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                    addOutput(new ASTOutput("data", viewInstance));
                                }
                            });
                        }

                        if (modelAttribute != null && viewAttribute == null) {
                            // With XForms NG we want lazy evaluation of the instance, so we should not force a
                            // read on the instance. We just connect the output.
                            addStatement(new ASTProcessorCall(XMLConstants.NULL_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(modelInstance)));
                                }
                            });
                        }

                        if (modelAttribute == null && viewAttribute == null) {
                            // Send out epilogue model data as a null document
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                    addOutput(new ASTOutput("data", epilogueModelData));
                                }
                            });
                        } else {
                            // Send out epilogue model data as produced by the model or used by the view
                            addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                {
                                    addInput(new ASTInput("data", new ASTHrefId(modelData)));
                                    addOutput(new ASTOutput("data", epilogueModelData));
                                }
                            });
                        }

                    }
                });
                addWhen(new ASTWhen() {
                    {
                        // There is a redirection due to the action

                        // With XForms NG we want lazy evaluation of the instance, so we should not force a
                        // read on the instance. We just connect the output.
                        addStatement(new ASTProcessorCall(XMLConstants.NULL_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", new ASTHrefId(xupdatedInstance)));
                            }
                        });
                        // Just connect the output
                        addStatement(new ASTProcessorCall(XMLConstants.NULL_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", new ASTHrefId(actionData)));
                            }
                        });
                        addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                addOutput(new ASTOutput("data", viewData));
                            }
                        });
                        addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                addOutput(new ASTOutput("data", epilogueModelData));
                            }
                        });
                        addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                                addOutput(new ASTOutput("data", viewInstance));
                            }
                        });
                    }
                });
            }
        });
    }

    private void executeResult(StepProcessorContext stepProcessorContext, final String controllerContext,
            ASTWhen when, final Map<String, String> pageIdToXFormsModel, final Map<String, String> pageIdToPathInfo,
            final Map<String, Document> pageIdToSetvaluesDocument, final ASTOutput paramedInstance,
            final Element resultElement, final ASTOutput actionData, final ASTOutput redirect,
            final ASTOutput xupdatedInstance, String instancePassing) {

        // Instance to update: either current, or instance from other page
        final String resultPageId = resultElement == null ? null : resultElement.attributeValue("page");
        Attribute instancePassingAttribute = resultElement == null ? null
                : resultElement.attribute("instance-passing");
        final String _instancePassing = instancePassingAttribute == null ? instancePassing
                : instancePassingAttribute.getValue();
        final String otherXForms = pageIdToXFormsModel.get(resultPageId);

        // Whether we use the legacy XUpdate transformation
        final boolean useLegacyTransformation = (resultElement != null && !resultElement.elements().isEmpty()
                && resultElement.attribute("transform") == null);

        // Whether we use the destination page's instance
        final boolean useCurrentPageInstance = resultPageId == null || otherXForms == null
                || !useLegacyTransformation;

        final ASTOutput instanceToUpdate;
        if (useCurrentPageInstance) {
            // We use the current page's submitted instance, if any.
            instanceToUpdate = paramedInstance;
        } else {
            // DEPRECATED: We use the resulting page's instance if possible (deprecated since xforms attribute on <page> is deprecated)
            instanceToUpdate = new ASTOutput("data", "other-page-instance");
            // Run the other page's XForms model
            final ASTOutput otherPageXFormsModel = new ASTOutput("data", "other-page-model");
            when.addStatement(
                    new StepProcessorCall(stepProcessorContext, controllerContext, otherXForms, "xforms-model") {
                        {
                            addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                            addInput(new ASTInput("instance", Dom4jUtils.NULL_DOCUMENT));
                            addInput(new ASTInput("xforms-model", Dom4jUtils.NULL_DOCUMENT));
                            addInput(new ASTInput("matcher", Dom4jUtils.NULL_DOCUMENT));
                            final ASTOutput dataOutput = new ASTOutput("data", otherPageXFormsModel);
                            final String[] locationParams = new String[] { "result page id", resultPageId,
                                    "result page XForms model", otherXForms };
                            dataOutput.setLocationData(
                                    new ExtendedLocationData((LocationData) resultElement.getData(),
                                            "reading other page XForms model data output", resultElement,
                                            locationParams, true));
                            addOutput(dataOutput);
                            setLocationData(new ExtendedLocationData((LocationData) resultElement.getData(),
                                    "executing other page XForms model", resultElement, locationParams, true));
                        }
                    });
            when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                {
                    addInput(new ASTInput("data",
                            new ASTHrefXPointer(new ASTHrefId(otherPageXFormsModel), EXTRACT_INSTANCE_XPATH)));
                    addOutput(new ASTOutput("data", instanceToUpdate));
                }
            });
        }

        // Create resulting instance
        final ASTOutput internalXUpdatedInstance;
        final boolean isTransformedInstance;
        if (resultElement != null && resultElement.attribute("transform") != null
                && !resultElement.elements().isEmpty()) {
            // Generic transform mechanism
            internalXUpdatedInstance = new ASTOutput("data", "internal-xupdated-instance");
            isTransformedInstance = true;

            final Document transformConfig = Dom4jUtils
                    .createDocumentCopyParentNamespaces((Element) resultElement.elements().get(0));
            final QName transformQName = Dom4jUtils.extractAttributeValueQName(resultElement, "transform");

            // Run transform
            final String resultTraceAttribute = resultElement.attributeValue("trace");
            when.addStatement(new ASTProcessorCall(transformQName) {
                {
                    addInput(new ASTInput("config", transformConfig));// transform
                    addInput(new ASTInput("instance", new ASTHrefId(paramedInstance)));// source-instance
                    addInput(new ASTInput("data", new ASTHrefId(instanceToUpdate)));// destination-instance
                    //addInput(new ASTInput("request-instance", new ASTHrefId(requestInstance)));// params-instance TODO
                    if (actionData != null)
                        addInput(new ASTInput("action", new ASTHrefId(actionData)));// action
                    else
                        addInput(new ASTInput("action", Dom4jUtils.NULL_DOCUMENT));// action
                    addOutput(new ASTOutput("data", internalXUpdatedInstance) {
                        {
                            setDebug(resultTraceAttribute);
                        }
                    });// updated-instance
                }
            });

        } else if (resultElement != null && !resultElement.elements().isEmpty()) {
            // Legacy transform mechanism (built-in XUpdate)
            internalXUpdatedInstance = new ASTOutput("data", "internal-xupdated-instance");
            isTransformedInstance = true;

            // Create XUpdate config
            // The code in this branch should be equivalent to the previous code doing the same
            // thing, except it will add the default namespace as well. I think there should be
            // the default namespace as well.
            final Document xupdateConfig = Dom4jUtils.createDocumentCopyParentNamespaces(resultElement);
            xupdateConfig.getRootElement().setQName(new QName("modifications", XUpdateConstants.XUPDATE_NAMESPACE));

            // Run XUpdate
            final String resultTraceAttribute = resultElement.attributeValue("trace");
            when.addStatement(new ASTProcessorCall(XMLConstants.XUPDATE_PROCESSOR_QNAME) {
                {
                    addInput(new ASTInput("config", xupdateConfig));
                    addInput(new ASTInput("data", new ASTHrefId(instanceToUpdate)));
                    addInput(new ASTInput("instance", new ASTHrefId(paramedInstance)));
                    if (actionData != null)
                        addInput(new ASTInput("action", new ASTHrefId(actionData)));
                    addOutput(new ASTOutput("data", internalXUpdatedInstance) {
                        {
                            setDebug(resultTraceAttribute);
                        }
                    });
                }
            });
        } else {
            internalXUpdatedInstance = instanceToUpdate;
            isTransformedInstance = false;
        }

        // Do redirect if we are going to a new page (NOTE: even if the new page has the same id as the current page)
        if (resultPageId != null) {
            final String forwardPathInfo = pageIdToPathInfo.get(resultPageId);
            if (forwardPathInfo == null)
                throw new OXFException("Cannot find page with id '" + resultPageId + "'");

            final Document setvaluesDocument = pageIdToSetvaluesDocument.get(resultPageId);
            final boolean doServerSideRedirect = _instancePassing != null
                    && _instancePassing.equals(INSTANCE_PASSING_FORWARD);
            final boolean doRedirectExitPortal = _instancePassing != null
                    && _instancePassing.equals(INSTANCE_PASSING_REDIRECT_PORTAL);

            // TODO: we should probably optimize all the redirect handling below with a dedicated processor
            {
                // Do redirect passing parameters from internalXUpdatedInstance without modifying URL
                final ASTOutput parametersOutput;
                if (isTransformedInstance) {
                    parametersOutput = new ASTOutput(null, "parameters");
                    // Pass parameters only if needed
                    final QName instanceToParametersProcessor = XMLConstants.INSTANCE_TO_PARAMETERS_PROCESSOR_QNAME;
                    when.addStatement(new ASTProcessorCall(instanceToParametersProcessor) {
                        {
                            addInput(new ASTInput("instance", new ASTHrefId(internalXUpdatedInstance)));
                            addInput(new ASTInput("filter",
                                    (setvaluesDocument != null) ? setvaluesDocument : Dom4jUtils.NULL_DOCUMENT));
                            addOutput(new ASTOutput("data", parametersOutput));
                        }
                    });
                } else {
                    parametersOutput = null;
                }
                // Handle path info
                final ASTOutput forwardPathInfoOutput = new ASTOutput(null, "forward-path-info");
                when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                    {
                        addInput(new ASTInput("data",
                                new NonLazyUserDataDocument(new NonLazyUserDataElement("path-info") {
                                    {
                                        setText(forwardPathInfo);
                                    }
                                })));
                        addOutput(new ASTOutput("data", forwardPathInfoOutput));
                    }
                });
                // Handle server-side redirect and exit portal redirect
                final ASTOutput isServerSideRedirectOutput = new ASTOutput(null, "is-server-side-redirect");
                when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                    {
                        addInput(new ASTInput("data",
                                new NonLazyUserDataDocument(new NonLazyUserDataElement("server-side") {
                                    {
                                        setText(Boolean.toString(doServerSideRedirect));
                                    }
                                })));
                        addOutput(new ASTOutput("data", isServerSideRedirectOutput));
                    }
                });
                final ASTOutput isRedirectExitPortal = new ASTOutput(null, "is-redirect-exit-portal");
                when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                    {
                        addInput(new ASTInput("data",
                                new NonLazyUserDataDocument(new NonLazyUserDataElement("exit-portal") {
                                    {
                                        setText(Boolean.toString(doRedirectExitPortal));
                                    }
                                })));
                        addOutput(new ASTOutput("data", isRedirectExitPortal));
                    }
                });
                // Aggregate redirect-url config
                final ASTHref redirectURLData;
                if (setvaluesDocument != null && isTransformedInstance) {
                    // Setvalues document - things are little more complicated, so we delegate
                    final ASTOutput redirectDataOutput = new ASTOutput(null, "redirect-data");

                    final ASTHrefAggregate redirectDataAggregate = new ASTHrefAggregate("redirect-url",
                            new ASTHrefId(forwardPathInfoOutput), new ASTHrefId(isServerSideRedirectOutput),
                            new ASTHrefId(isRedirectExitPortal));
                    redirectDataAggregate.getHrefs().add(new ASTHrefId(parametersOutput));

                    when.addStatement(new ASTProcessorCall(XMLConstants.UNSAFE_XSLT_PROCESSOR_QNAME) {
                        {
                            addInput(new ASTInput("config", new ASTHrefURL(REVERSE_SETVALUES_XSL)));
                            addInput(new ASTInput("data", redirectDataAggregate));
                            addInput(new ASTInput("instance", new ASTHrefId(internalXUpdatedInstance)));
                            addInput(new ASTInput("setvalues", setvaluesDocument));
                            addOutput(new ASTOutput("data", redirectDataOutput));
                        }
                    });
                    redirectURLData = new ASTHrefId(redirectDataOutput);
                } else {
                    // No setvalues document, we can simply aggregate with XPL
                    final ASTHrefAggregate redirectDataAggregate = new ASTHrefAggregate("redirect-url",
                            new ASTHrefId(forwardPathInfoOutput), new ASTHrefId(isServerSideRedirectOutput),
                            new ASTHrefId(isRedirectExitPortal));
                    if (isTransformedInstance) // Pass parameters only if needed
                        redirectDataAggregate.getHrefs().add(new ASTHrefId(parametersOutput));
                    redirectURLData = redirectDataAggregate;
                }
                // Execute the redirect
                when.addStatement(new ASTProcessorCall(XMLConstants.REDIRECT_PROCESSOR_QNAME) {
                    {
                        addInput(new ASTInput("data", redirectURLData));// {{setDebug("redirect 2");}}
                        final String[] locationParams = new String[] { "result page id", resultPageId };
                        setLocationData(new ExtendedLocationData((LocationData) resultElement.getData(),
                                "page redirection", resultElement, locationParams, true));
                    }
                });
            }
        }

        // Signal if we did a redirect
        when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
            {
                addInput(
                        new ASTInput("data", new NonLazyUserDataDocument(new NonLazyUserDataElement("is-redirect") {
                            {
                                setText(Boolean.toString(resultPageId != null));
                            }
                        })));
                addOutput(new ASTOutput("data", redirect));
            }
        });

        // Export XUpdated instance from this branch
        when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
            {
                addInput(new ASTInput("data", new ASTHrefId(internalXUpdatedInstance)));
                addOutput(new ASTOutput("data", xupdatedInstance));
            }
        });
    }

    /**
     * Handle <files>
     */
    private void handleFile(ASTWhen when, final ASTOutput request, final String mimeType,
            final ASTOutput epilogueData, final ASTOutput epilogueModelData, final ASTOutput epilogueInstance) {
        when.addStatement(new ASTProcessorCall(XMLConstants.RESOURCE_SERVER_PROCESSOR_QNAME) {
            {

                addInput(new ASTInput("config", new ASTHrefAggregate("path",
                        new ASTHrefXPointer(new ASTHrefId(request), "string(/request/request-path)"))));

                if (mimeType == null) {
                    addInput(
                            new ASTInput(ResourceServer.MIMETYPE_INPUT, new ASTHrefURL("oxf:/oxf/mime-types.xml")));
                } else {
                    final Document mimeTypeConfig = new NonLazyUserDataDocument(
                            new NonLazyUserDataElement("mime-types") {
                                {
                                    add(new NonLazyUserDataElement("mime-type") {
                                        {
                                            add(new NonLazyUserDataElement("name") {
                                                {
                                                    addText(mimeType);
                                                }
                                            });
                                            add(new NonLazyUserDataElement("pattern") {
                                                {
                                                    addText("*");
                                                }
                                            });
                                        }
                                    });
                                }
                            });
                    addInput(new ASTInput(ResourceServer.MIMETYPE_INPUT, mimeTypeConfig));
                }
            }
        });
        when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
            {
                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                addOutput(new ASTOutput("data", epilogueData));
            }
        });
        when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
            {
                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                addOutput(new ASTOutput("data", epilogueModelData));
            }
        });
        when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
            {
                addInput(new ASTInput("data", Dom4jUtils.NULL_DOCUMENT));
                addOutput(new ASTOutput("data", epilogueInstance));
            }
        });
    }

    /**
     * Creates a single StepProcessor. This should be called only once for a given Page Flow
     * configuration. Then the same StepProcessor should be used for each step.
     */
    private static class StepProcessorContext {

        private PipelineConfig pipelineConfig;

        private StepProcessorContext(final Object controllerValidity) {
            this.pipelineConfig = PipelineProcessor.createConfigFromAST(new ASTPipeline() {
                {
                    setValidity(controllerValidity);

                    final ASTParam stepURLInput = addParam(new ASTParam(ASTParam.INPUT, "step-url"));
                    final ASTParam stepTypeInput = addParam(new ASTParam(ASTParam.INPUT, "step-type"));
                    final ASTParam dataInput = addParam(new ASTParam(ASTParam.INPUT, "data"));
                    final ASTParam instanceInput = addParam(new ASTParam(ASTParam.INPUT, "instance"));
                    final ASTParam xformsModelInput = addParam(new ASTParam(ASTParam.INPUT, "xforms-model"));
                    final ASTParam matcherInput = addParam(new ASTParam(ASTParam.INPUT, "matcher"));
                    final ASTParam dataOutput = addParam(new ASTParam(ASTParam.OUTPUT, "data"));
                    final ASTParam instanceOutput = addParam(new ASTParam(ASTParam.OUTPUT, "instance"));

                    // Rewrite the URL if needed
                    final ASTOutput rewroteStepURL = new ASTOutput(null, "rewrote-step-url");
                    addStatement(new ASTChoose(new ASTHrefId(stepURLInput)) {
                        {
                            addWhen(new ASTWhen("contains(/config/url, '${')") {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.XSLT_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefAggregate("root",
                                                    new ASTHrefId(stepURLInput), new ASTHrefId(matcherInput))));
                                            addInput(new ASTInput("config", new ASTHrefURL(REWRITE_XSL)));
                                            addOutput(new ASTOutput("data", rewroteStepURL));
                                        }
                                    });
                                }
                            });
                            addWhen(new ASTWhen() {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(stepURLInput)));
                                            addOutput(new ASTOutput("data", rewroteStepURL));
                                        }
                                    });
                                }
                            });
                        }
                    });

                    final ASTOutput contentXIncluded = new ASTOutput("data", "content-xincluded");
                    {
                        // Read file to "execute"
                        final ASTOutput content = new ASTOutput("data", "content");
                        addStatement(new ASTProcessorCall(XMLConstants.URL_GENERATOR_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("config", new ASTHrefId(rewroteStepURL)));
                                addOutput(content);
                            }
                        });

                        // Insert XInclude processor to process content with XInclude
                        addStatement(new ASTProcessorCall(XMLConstants.XINCLUDE_PROCESSOR_QNAME) {
                            {
                                addInput(new ASTInput("config", new ASTHrefId(content)));
                                addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                final ASTOutput contentOutput = new ASTOutput("data", contentXIncluded);
                                addOutput(contentOutput);
                            }
                        });
                    }

                    final ASTOutput resultData = new ASTOutput(null, "result-data");
                    final ASTOutput resultInstance = new ASTOutput(null, "result-instance");

                    // Perform verifications on input/output of XPL
                    addStatement(new ASTChoose(new ASTHrefId(stepTypeInput)) {
                        {
                            addWhen(new ASTWhen("/step-type = 'view'") {
                                {
                                    // We are dealing with a view
                                    addStatement(new ASTChoose(new ASTHrefId(contentXIncluded)) {
                                        {
                                            addWhen(new ASTWhen(
                                                    "namespace-uri(/*) = 'http://www.orbeon.com/oxf/pipeline' "
                                                            + "and count(/*/*[local-name() = 'param' and @type = 'output' and @name = 'data']) = 0") {
                                                {
                                                    // The XPL has not data output
                                                    addStatement(new ASTProcessorCall(
                                                            XMLConstants.ERROR_PROCESSOR_QNAME) {
                                                        {
                                                            final Document errorDocument = new NonLazyUserDataDocument(
                                                                    new NonLazyUserDataElement("error"));
                                                            errorDocument.getRootElement()
                                                                    .addText("XPL view must have a 'data' output");
                                                            addInput(new ASTInput("config", errorDocument));
                                                        }
                                                    });
                                                }
                                            });
                                        }
                                    });
                                }
                            });
                        }
                    });

                    addStatement(new ASTChoose(new ASTHrefId(contentXIncluded)) {
                        {

                            // XPL file with instance & data output
                            addWhen(new ASTWhen("namespace-uri(/*) = 'http://www.orbeon.com/oxf/pipeline' "
                                    + "and /*/*[local-name() = 'param' and @type = 'output' and @name = 'data'] "
                                    + "and /*/*[local-name() = 'param' and @type = 'output' and @name = 'instance']") {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("config", new ASTHrefId(contentXIncluded)));
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                            addInput(new ASTInput("xforms-model", new ASTHrefId(xformsModelInput)));
                                            final ASTOutput datOut = new ASTOutput("data", resultData);
                                            addOutput(datOut);
                                            final ASTOutput instOut = new ASTOutput("instance", resultInstance);
                                            addOutput(instOut);
                                        }
                                    });
                                }
                            });

                            // XPL file with only data output
                            addWhen(new ASTWhen("namespace-uri(/*) = 'http://www.orbeon.com/oxf/pipeline' "
                                    + "and /*/*[local-name() = 'param' and @type = 'output' and @name = 'data']") {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("config", new ASTHrefId(contentXIncluded)));
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                            addInput(new ASTInput("xforms-model", new ASTHrefId(xformsModelInput)));
                                            final ASTOutput datOut = new ASTOutput("data", resultData);
                                            addOutput(datOut);
                                        }
                                    });
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(instanceInput)));
                                            final ASTOutput datOut = new ASTOutput("data", resultInstance);
                                            addOutput(datOut);
                                        }
                                    });
                                }
                            });

                            // XPL file with only instance output
                            addWhen(new ASTWhen("namespace-uri(/*) = 'http://www.orbeon.com/oxf/pipeline' "
                                    + "and /*/*[local-name() = 'param' and @type = 'output' and @name = 'instance']") {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("config", new ASTHrefId(contentXIncluded)));
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                            addInput(new ASTInput("xforms-model", new ASTHrefId(xformsModelInput)));
                                            final ASTOutput instOut = new ASTOutput("instance", resultInstance);
                                            addOutput(instOut);
                                        }
                                    });
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            final ASTOutput resDatOut = new ASTOutput("data", resultData);
                                            addOutput(resDatOut);
                                        }
                                    });
                                }
                            });

                            // XPL file with no output
                            addWhen(new ASTWhen("namespace-uri(/*) = 'http://www.orbeon.com/oxf/pipeline'") {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("config", new ASTHrefId(contentXIncluded)));
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                            addInput(new ASTInput("xforms-model", new ASTHrefId(xformsModelInput)));
                                        }
                                    });
                                    // Simply bypass data and instance channels
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            final ASTOutput resDatOut = new ASTOutput("data", resultData);
                                            addOutput(resDatOut);
                                        }
                                    });
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(instanceInput)));
                                            final ASTOutput resInstOut = new ASTOutput("data", resultInstance);
                                            addOutput(resInstOut);
                                        }
                                    });
                                }
                            });

                            // PFC file (should only work as model)
                            addWhen(new ASTWhen("namespace-uri(/*) = 'http://www.orbeon.com/oxf/controller'") {
                                {
                                    addStatement(new ASTProcessorCall(XMLConstants.PAGE_FLOW_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("controller", new ASTHrefId(contentXIncluded)));
                                        }
                                    });
                                    // Simply bypass data and instance channels
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                            final ASTOutput resDatOut = new ASTOutput("data", resultData);
                                            addOutput(resDatOut);
                                        }
                                    });
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(instanceInput)));
                                            final ASTOutput resInstOut = new ASTOutput("data", resultInstance);
                                            addOutput(resInstOut);
                                        }
                                    });
                                }
                            });

                            // XSLT file (including XSLT 2.0 "Simplified Stylesheet Modules")
                            addWhen(new ASTWhen(
                                    "namespace-uri(/*) = 'http://www.w3.org/1999/XSL/Transform' or /*/@xsl:version = '2.0'") {
                                {
                                    setNamespaces(NAMESPACES_WITH_XSI_AND_XSLT);

                                    // Copy the instance as is
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(instanceInput)));
                                            final ASTOutput resInstOut = new ASTOutput("data", resultInstance);
                                            addOutput(resInstOut);
                                        }
                                    });

                                    // Process XInclude
                                    //                        final ASTOutput xincludedContent = new ASTOutput("data", "xincluded-content");
                                    //                        addStatement(new ASTProcessorCall(XMLConstants.XINCLUDE_PROCESSOR_QNAME) {{
                                    //                            addInput(new ASTInput("config", new ASTHrefId(content)));
                                    ////                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                    ////                            addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                    //                            addOutput(xincludedContent);
                                    //                        }});

                                    addStatement(new ASTChoose(new ASTHrefId(contentXIncluded)) {

                                        private void addXSLTWhen(final String condition,
                                                final QName processorQName) {
                                            addWhen(new ASTWhen(condition) {
                                                {
                                                    setNamespaces(NAMESPACES_WITH_XSI_AND_XSLT);

                                                    // Changed from <= 2.8 behavior
                                                    addStatement(new ASTProcessorCall(processorQName) {
                                                        {
                                                            addInput(new ASTInput("config",
                                                                    new ASTHrefId(contentXIncluded)));
                                                            addInput(
                                                                    new ASTInput("data", new ASTHrefId(dataInput)));
                                                            addInput(new ASTInput("instance",
                                                                    new ASTHrefId(instanceInput)));
                                                            final ASTOutput resDatOut = new ASTOutput("data",
                                                                    resultData);
                                                            addOutput(resDatOut);
                                                        }
                                                    });
                                                }
                                            });
                                        }

                                        {
                                            // XSLT 1.0: There is no xsl:version = '2.0' attribute (therefore the namespace of the
                                            //           root element is xsl as per the condition above) and the version attribute
                                            //           is exactly '1.0'
                                            addXSLTWhen("not(/*/@xsl:version = '2.0') and /*/@version = '1.0'",
                                                    XMLConstants.PFC_XSLT10_PROCESSOR_QNAME);

                                            // XSLT 2.0: There is an xsl:version = '2.0' attribute or the namespace or the root
                                            //           element is xsl and the version is different from '1.0'
                                            addXSLTWhen(null, XMLConstants.PFC_XSLT20_PROCESSOR_QNAME);
                                        }
                                    });
                                }
                            });

                            // XML file
                            addWhen(new ASTWhen() {
                                {

                                    // Copy the instance as is
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(instanceInput)));
                                            final ASTOutput resInstOut = new ASTOutput("data", resultInstance);
                                            addOutput(resInstOut);
                                        }
                                    });

                                    // Copy the data as is
                                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                                        {
                                            addInput(new ASTInput("data", new ASTHrefId(contentXIncluded)));
                                            final ASTOutput resDatOut = new ASTOutput("data", resultData);
                                            addOutput(resDatOut);
                                        }
                                    });

                                    // Insert XInclude processor to process static XML file with XInclude
                                    //                        addStatement(new ASTProcessorCall(XMLConstants.XINCLUDE_PROCESSOR_QNAME) {{
                                    //                            addInput(new ASTInput("config", new ASTHrefId(content)));
                                    //                            addInput(new ASTInput("data", new ASTHrefId(dataInput)));
                                    //                            addInput(new ASTInput("instance", new ASTHrefId(instanceInput)));
                                    //                            final ASTOutput resDatOut = new ASTOutput("data", resultData);
                                    //                            addOutput(resDatOut);
                                    //                        }});
                                }
                            });
                        }
                    });

                    // Connect results
                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                        {
                            addInput(new ASTInput("data", new ASTHrefId(resultData)));
                            addOutput(new ASTOutput("data", dataOutput));
                        }
                    });
                    addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {
                        {
                            addInput(new ASTInput("data", new ASTHrefId(resultInstance)));
                            final ASTOutput resDatOut = new ASTOutput("data", instanceOutput);
                            addOutput(resDatOut);
                        }
                    });
                }
            });
        }

        public PipelineConfig getPipelineConfig() {
            return pipelineConfig;
        }
    }

    private static class StepProcessorCall extends ASTProcessorCall {
        public StepProcessorCall(StepProcessorContext stepProcessorContext, String controllerContext, String uri,
                String stepType) {
            super(new PipelineProcessor(stepProcessorContext.getPipelineConfig()));
            try {
                // Create document and input for URI
                final String url = URLFactory.createURL(controllerContext, uri).toExternalForm();
                final Document configDocument;
                {
                    configDocument = new NonLazyUserDataDocument(new NonLazyUserDataElement("config"));
                    final Element urlElement = configDocument.getRootElement().addElement("url");
                    urlElement.addText(url);
                    final Element handleXIncludeElement = configDocument.getRootElement()
                            .addElement("handle-xinclude");
                    handleXIncludeElement.addText("false");
                    // Allow external entities in document
                    final Element externalEntitiesElement = configDocument.getRootElement()
                            .addElement("external-entities");
                    externalEntitiesElement.addText("true");
                }

                addInput(new ASTInput("step-url", configDocument));
                // Create document and input for step type
                final Document stepTypeDocument = new NonLazyUserDataDocument(
                        new NonLazyUserDataElement("step-type"));
                stepTypeDocument.getRootElement().addText(stepType);
                addInput(new ASTInput("step-type", stepTypeDocument));
            } catch (MalformedURLException e) {
                throw new OXFException(e);
            }
        }
    }

    private static class PageFlow {
        private PipelineProcessor pipelineProcessor;
        private List<URLRewriterUtils.PathMatcher> pathMatchers;

        public PageFlow(PipelineProcessor pipelineProcessor, List<URLRewriterUtils.PathMatcher> pathMatchers) {
            this.pipelineProcessor = pipelineProcessor;
            this.pathMatchers = pathMatchers;
        }

        public PipelineProcessor getPipelineProcessor() {
            return pipelineProcessor;
        }

        public List<URLRewriterUtils.PathMatcher> getPathMatchers() {
            return pathMatchers;
        }
    }
}