/**
* 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.controller;
import org.orbeon.dom.*;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
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.resources.URLFactory;
import org.orbeon.oxf.xml.NamespaceMapping;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.ExtendedLocationData;
import org.orbeon.oxf.xml.dom4j.LocationData;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// PageFlowControllerProcessor code that hasn't been migrated to Scala yet
public class PageFlowControllerBuilder {
public final static NamespaceMapping NAMESPACES_WITH_XSI_AND_XSLT;
// 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
public final static String INSTANCE_PASSING_REDIRECT = "redirect";
public final static String INSTANCE_PASSING_FORWARD = "forward";
public final static String INSTANCE_PASSING_REDIRECT_PORTAL = "redirect-exit-portal";
static {
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 static void handleEpilogue(final String controllerContext, List<ASTStatement> statements, final String epilogueURL, final Element epilogueElement,
final ASTOutput epilogueData, final ASTOutput epilogueModelData, final ASTOutput epilogueInstance) {
if (epilogueURL == null) {
// Run HTML serializer if no epilogue is specified
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 = DocumentFactory.createDocument("config");
Element rootElement = config.getRootElement();
rootElement.addElement("version").addText("5.0");
rootElement.addElement("encoding").addText("utf-8");
addInput(new ASTInput("config", config));
addInput(new ASTInput("data", new ASTHrefId(epilogueData)));
//setLocationData(TODO);
}});
}});
addWhen(new ASTWhen());
}});
} else {
// Send result through epilogue
statements.add(new ASTProcessorCall(XMLConstants.PIPELINE_PROCESSOR_QNAME) {{
final String url = URLFactory.createURL(controllerContext, epilogueURL).toExternalForm();
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));
}});
}
}
public static Document getSetValuesDocument(final Element pageElement) {
final List setValueElements = pageElement.elements("setvalue");
final Document setvaluesDocument;
if (!setValueElements.isEmpty()) {
// Create document with setvalues
setvaluesDocument = DocumentFactory.createDocument("params");
// 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>
*/
public static void handlePage(final StepProcessorContext stepProcessorContext, final String urlBase,
List<ASTStatement> statementsList, final Element pageElement,
final String matcherOutputOrParamId,
final ASTOutput viewData, final ASTOutput epilogueModelData, final ASTOutput viewInstance,
final Map<String, String> pageIdToPathInfo,
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 = URLFactory.createURL(urlBase, defaultSubmissionAttribute).toExternalForm();
final Document configDocument = DocumentFactory.createDocument("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 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(matcherOutputOrParamId)));
} 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 = DocumentFactory.createDocument("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; {
// NOTE: Prior to 2012-06-27, the XSD schema would put a default value of true()
final String tempWhen = actionElement.attributeValue("when");
if (tempWhen == null)
whenAttribute = "true()";
else
whenAttribute = tempWhen;
};
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-" + 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, urlBase, 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(matcherOutputOrParamId)));
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));
addOutput(dataOutput);
setLocationData(new ExtendedLocationData((LocationData) actionElement.getData(), "executing action", pageElement, locationParams));
}});
// 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));
}});
}
if (actionElement.elements("result").size() > 0 && internalActionData != null) {
// At least one result testing on action
// Test based on action
addStatement(new ASTChoose(new ASTHrefId(internalActionData)) {{
for (Object o: actionElement.elements("result")) {
final Element resultElement = (Element) o;
final String resultWhenAttribute; {
// NOTE: Prior to 2012-06-27, the XSD schema would put a default value of true()
final String tempWhen = resultElement.attributeValue("when");
if (tempWhen == null)
resultWhenAttribute = "true()";
else
resultWhenAttribute = tempWhen;
};
// 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));
}
executeResult(this, 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", DocumentFactory.createDocument(DocumentFactory.createElementWithText("is-redirect", "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 {
// No result or result not depending on action
final Element resultElement = actionElement.element("result");
executeResult(this, pageIdToPathInfo, pageIdToSetvaluesDocument, xformedInstance, resultElement,
internalActionData, isRedirect, xupdatedInstance, instancePassing);
}
}});
}
if (!foundActionWithoutWhen[0]) {
// Default 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 = DocumentFactory.createDocument("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, urlBase, 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(matcherOutputOrParamId)));
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));
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));
}
setLocationData(new ExtendedLocationData((LocationData) pageElement.getData(), "executing page model", pageElement, locationParams));
}});
} 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, urlBase, 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(matcherOutputOrParamId)));
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));
addOutput(dataOutput);
}
{
final ASTOutput instanceOutput = new ASTOutput("instance", viewInstance);
instanceOutput.setLocationData(new ExtendedLocationData((LocationData) pageElement.getData(), "reading page view instance output", pageElement, locationParams));
addOutput(instanceOutput);
}
setLocationData(new ExtendedLocationData((LocationData) pageElement.getData(), "executing page view", pageElement, locationParams));
}});
} 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 static void executeResult(ASTWhen when,
final Map<String, String> pageIdToPathInfo, final Map<String, Document> pageIdToSetvaluesDocument,
final ASTOutput instanceToUpdate, 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();
// 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(instanceToUpdate)));// 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 {
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
when.addStatement(new ASTProcessorCall(XMLConstants.INSTANCE_TO_PARAMETERS_PROCESSOR_QNAME) {{
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", DocumentFactory.createDocument(DocumentFactory.createElementWithText("path-info", 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", DocumentFactory.createDocument(DocumentFactory.createElementWithText("server-side", 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", DocumentFactory.createDocument(DocumentFactory.createElementWithText("exit-portal", 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));
}});
}
}
// Signal if we did a redirect
when.addStatement(new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME) {{
addInput(new ASTInput("data", DocumentFactory.createDocument(DocumentFactory.createElementWithText("is-redirect", 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));
}});
}
/**
* 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.
*/
public static class StepProcessorContext {
private PipelineConfig pipelineConfig;
public 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 = DocumentFactory.createDocument("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()));
// Create document and input for URI
final String url = URLFactory.createURL(controllerContext, uri).toExternalForm();
final Document configDocument;
{
configDocument = DocumentFactory.createDocument("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 = DocumentFactory.createDocument("step-type");
stepTypeDocument.getRootElement().addText(stepType);
addInput(new ASTInput("step-type", stepTypeDocument));
}
}
}