/** * 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.xforms.action; import org.apache.commons.lang3.StringUtils; import org.orbeon.dom.Element; import org.orbeon.dom.QName; import org.orbeon.oxf.common.OrbeonLocationException; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.util.IndentedLogger; import org.orbeon.oxf.util.XPathCache; import org.orbeon.oxf.xforms.*; import org.orbeon.oxf.xforms.analysis.ElementAnalysis; import org.orbeon.oxf.xforms.analysis.controls.ActionTrait; import org.orbeon.oxf.xforms.event.Dispatch; import org.orbeon.oxf.xforms.event.XFormsEvent; import org.orbeon.oxf.xforms.event.XFormsEventObserver; import org.orbeon.oxf.xforms.function.XFormsFunction; import org.orbeon.oxf.xforms.model.XFormsModel; import org.orbeon.oxf.xforms.xbl.Scope; import org.orbeon.oxf.xforms.xbl.XBLContainer; import org.orbeon.oxf.xml.NamespaceMapping; import org.orbeon.oxf.xml.dom4j.ExtendedLocationData; import org.orbeon.oxf.xml.dom4j.LocationData; import org.orbeon.saxon.om.Item; import org.orbeon.saxon.value.BooleanValue; import scala.Option; import java.util.Collections; import java.util.List; /** * Execute a top-level XForms action and the included nested actions if any. */ public class XFormsActionInterpreter { private final IndentedLogger _indentedLogger; private final XBLContainer _container; private final XFormsContainingDocument _containingDocument; private final XFormsContextStack _actionXPathContext; public final Element outerActionElement; private final String handlerEffectiveId; public final XFormsEvent event; public final XFormsEventObserver eventObserver; public XFormsActionInterpreter(XBLContainer container, XFormsContextStack actionXPathContext, Element outerActionElement, String handlerEffectiveId, XFormsEvent event, XFormsEventObserver eventObserver) { this._container = container; this._containingDocument = container.getContainingDocument(); this._indentedLogger = _containingDocument.getIndentedLogger(XFormsActions.LOGGING_CATEGORY()); this._actionXPathContext = actionXPathContext; this.outerActionElement = outerActionElement; this.handlerEffectiveId = handlerEffectiveId; this.event = event; this.eventObserver = eventObserver; } public IndentedLogger indentedLogger() { return _indentedLogger; } public XBLContainer container() { return _container; } public XFormsContainingDocument containingDocument() { return _containingDocument; } public XFormsContextStack actionXPathContext() { return _actionXPathContext; } /** * Return the namespace mappings for the given action element. * * @param actionElement element to get namespace mapping for * @return mapping */ public NamespaceMapping getNamespaceMappings(Element actionElement) { return _container.getNamespaceMappings(actionElement); } /** * Execute an XForms action. */ public void runAction(ElementAnalysis actionAnalysis) { final Element actionElement = actionAnalysis.element(); final ActionTrait actionTrait = (ActionTrait) actionAnalysis; try { // Condition final String ifConditionAttribute = actionTrait.ifConditionJava(); final String whileIterationAttribute = actionTrait.whileConditionJava(); final String iterateIterationAttribute = actionTrait.iterateJava(); // Push @iterate (if present) within the @model and @context context final NamespaceMapping namespaceMapping = actionAnalysis.namespaceMapping(); // TODO: function context _actionXPathContext.pushBinding( iterateIterationAttribute, actionAnalysis.contextJava(), null, actionAnalysis.modelJava(), null, actionElement, namespaceMapping, getSourceEffectiveId(actionElement), actionAnalysis.scope(), false ); // NOTE: At this point, the context has already been set to the current action element if (iterateIterationAttribute != null) { // Gotta iterate // NOTE: It's not 100% how @context and @iterate should interact here. Right now @iterate overrides @context, // i.e. @context is evaluated first, and @iterate sets a new context for each iteration { final List<Item> currentNodeset = _actionXPathContext.getCurrentBindingContext().nodeset(); final int iterationCount = currentNodeset.size(); for (int index = 1; index <= iterationCount; index++) { // Push iteration _actionXPathContext.pushIteration(index); final Item overriddenContextNodeInfo = currentNodeset.get(index - 1); runSingleIteration(actionAnalysis, actionElement.getQName(), ifConditionAttribute, whileIterationAttribute, true, overriddenContextNodeInfo); // Restore context _actionXPathContext.popBinding(); } } } else { // Do a single iteration run (but this may repeat over the @while condition!) runSingleIteration(actionAnalysis, actionElement.getQName(), ifConditionAttribute, whileIterationAttribute, _actionXPathContext.getCurrentBindingContext().hasOverriddenContext(), _actionXPathContext.getCurrentBindingContext().contextItem()); } // Restore _actionXPathContext.popBinding(); } catch (Exception e) { throw OrbeonLocationException.wrapException(e, new ExtendedLocationData((LocationData) actionElement.getData(), "running XForms action", actionElement, new String[]{"action name", actionElement.getQName().getQualifiedName()})); } } private void runSingleIteration(ElementAnalysis actionAnalysis, QName actionQName, String ifConditionAttribute, String whileIterationAttribute, boolean hasOverriddenContext, Item contextItem) { // The context is now the overridden context int whileIteration = 1; while (true) { // Check if the conditionAttribute attribute exists and stop if false if (ifConditionAttribute != null) { boolean result = evaluateCondition(actionAnalysis.element(), actionQName.getQualifiedName(), ifConditionAttribute, "if", contextItem); if (!result) break; } // Check if the iterationAttribute attribute exists and stop if false if (whileIterationAttribute != null) { boolean result = evaluateCondition(actionAnalysis.element(), actionQName.getQualifiedName(), whileIterationAttribute, "while", contextItem); if (!result) break; } // We are executing the action if (_indentedLogger.isDebugEnabled()) { _indentedLogger.startHandleOperation("interpreter", "executing", "action name", actionQName.getQualifiedName(), "while iteration", (whileIterationAttribute != null) ? Integer.toString(whileIteration) : null ); } // Get action and execute it final DynamicActionContext dynamicActionContext = new DynamicActionContext(this, actionAnalysis, hasOverriddenContext ? Option.apply(contextItem) : Option.apply((Item) null)); // Push binding excluding excluding @context and @model // NOTE: If we repeat, re-evaluate the action binding. // For example: // // <xf:delete ref="/*/foo[1]" while="/*/foo"/> // // In this case, in the second iteration, xf:repeat must find an up-to-date nodeset! // TODO: function context _actionXPathContext.pushBinding( actionAnalysis.refJava(), null, null, null, actionAnalysis.bindJava(), actionAnalysis.element(), actionAnalysis.namespaceMapping(), getSourceEffectiveId(actionAnalysis.element()), actionAnalysis.scope(), false ); XFormsActions.getAction(actionQName).execute(dynamicActionContext); _actionXPathContext.popBinding(); if (_indentedLogger.isDebugEnabled()) { _indentedLogger.endHandleOperation( "action name", actionQName.getQualifiedName(), "while iteration", (whileIterationAttribute != null) ? Integer.toString(whileIteration) : null); } // Stop if there is no iteration if (whileIterationAttribute == null) break; whileIteration++; } } private boolean evaluateCondition(Element actionElement, String actionName, String conditionAttribute, String conditionType, Item contextItem) { // Execute condition relative to the overridden context if it exists, or the in-scope context if not final List<Item> contextNodeset; final int contextPosition; { if (contextItem != null) { // Use provided context item contextNodeset = Collections.singletonList(contextItem); contextPosition = 1; } else { // Use empty context contextNodeset = XFormsConstants.EMPTY_ITEM_LIST; contextPosition = 0; } } // Don't evaluate the condition if the context has gone missing { if (contextNodeset.size() == 0) {// || containingDocument.getInstanceForNode((NodeInfo) contextNodeset.get(contextPosition - 1)) == null if (_indentedLogger.isDebugEnabled()) _indentedLogger.logDebug("interpreter", "not executing", "action name", actionName, "condition type", conditionType, "reason", "missing context"); return false; } } final List<Item> conditionResult = evaluateKeepItems(actionElement, contextNodeset, contextPosition, "boolean(" + conditionAttribute + ")"); if (! ((BooleanValue) conditionResult.get(0)).effectiveBooleanValue()) { // Don't execute action if (_indentedLogger.isDebugEnabled()) _indentedLogger.logDebug("interpreter", "not executing", "action name", actionName, "condition type", conditionType, "reason", "condition evaluated to 'false'", "condition", conditionAttribute); return false; } else { // Condition is true return true; } } /** * Return the source against which id resolutions are made for the given action element. * * @param actionElement action element to resolve * @return effective id of source */ public String getSourceEffectiveId(Element actionElement) { return XFormsUtils.getRelatedEffectiveId(handlerEffectiveId, getActionStaticId(actionElement)); } /** * Evaluate an expression as a string. This returns "" if the result is an empty sequence. */ public String evaluateAsString(Element actionElement, List<Item> nodeset, int position, String xpathExpression) { final XFormsFunction.Context functionContext = _actionXPathContext.getFunctionContext(getSourceEffectiveId(actionElement)); // @ref points to something final String result = XPathCache.evaluateAsString( nodeset, position, xpathExpression, getNamespaceMappings(actionElement), _actionXPathContext.getCurrentBindingContext().getInScopeVariables(), _containingDocument.getFunctionLibrary(), functionContext, null, (LocationData) actionElement.getData(), containingDocument().getRequestStats().getReporter()); return result != null ? result : ""; } public List<Item> evaluateKeepItems(Element actionElement, List<Item> nodeset, int position, String xpathExpression) { final XFormsFunction.Context functionContext = _actionXPathContext.getFunctionContext(getSourceEffectiveId(actionElement)); // @ref points to something return XPathCache.evaluateKeepItems( nodeset, position, xpathExpression, getNamespaceMappings(actionElement), _actionXPathContext.getCurrentBindingContext().getInScopeVariables(), _containingDocument.getFunctionLibrary(), functionContext, null, (LocationData) actionElement.getData(), containingDocument().getRequestStats().getReporter()); } /** * Resolve a value which may be an AVT. * * @param actionElement action element * @param attributeValue raw value to resolve * @return resolved attribute value, null if the value is null or if the XPath context item is missing */ public String resolveAVTProvideValue(Element actionElement, String attributeValue) { if (attributeValue == null) return null; // Whether this can't be an AVT final String resolvedAVTValue; if (XFormsUtils.maybeAVT(attributeValue)) { // We have to go through AVT evaluation final BindingContext bindingContext = _actionXPathContext.getCurrentBindingContext(); // We don't have an evaluation context so return // CHECK: In the future we want to allow an empty evaluation context so do we really want this check? if (bindingContext.singleItemOpt().isEmpty()) return null; final NamespaceMapping namespaceMapping = getNamespaceMappings(actionElement); final LocationData locationData = (LocationData) actionElement.getData(); final XFormsFunction.Context functionContext = _actionXPathContext.getFunctionContext(getSourceEffectiveId(actionElement)); resolvedAVTValue = XPathCache.evaluateAsAvt( bindingContext.nodeset(), bindingContext.position(), attributeValue, namespaceMapping, _actionXPathContext.getCurrentBindingContext().getInScopeVariables(), _containingDocument.getFunctionLibrary(), functionContext, null, locationData, containingDocument().getRequestStats().getReporter()); } else { // We optimize as this doesn't need AVT evaluation resolvedAVTValue = attributeValue; } return resolvedAVTValue; } /** * Resolve the value of an attribute which may be an AVT. * * @param actionElement action element * @param attributeName QName of the attribute containing the value * @return resolved attribute value */ public String resolveAVT(Element actionElement, QName attributeName) { return resolveAVTProvideValue(actionElement, actionElement.attributeValue(attributeName)); } /** * Resolve the value of an attribute which may be an AVT. * * @param actionElement action element * @param attributeName name of the attribute containing the value * @return resolved attribute value */ public String resolveAVT(Element actionElement, String attributeName) { // Get raw attribute value final String attributeValue = actionElement.attributeValue(attributeName); if (attributeValue == null) return null; return resolveAVTProvideValue(actionElement, attributeValue); } /** * Find an effective object based on either the xxf:repeat-indexes attribute, or on the current repeat indexes. * * @param actionElement current action element * @param targetStaticOrAbsoluteId static id or absolute id of the target to resolve * @return effective control if found */ public XFormsObject resolveObject(Element actionElement, String targetStaticOrAbsoluteId) { // First resolve the object by static id final scala.Option<XFormsObject> result = _container.resolveObjectByIdInScope(getSourceEffectiveId(actionElement), targetStaticOrAbsoluteId, Option.<Item>apply(null)); if (result.isEmpty()) { return null; } else { // Get indexes as space-separated list final String repeatIndexes = resolveAVT(actionElement, XFormsConstants.XXFORMS_REPEAT_INDEXES_QNAME); if (StringUtils.isBlank(repeatIndexes)) { // Most common case: just return the resolved object return result.get(); } else { // Extension: repeat indexes are provided final String effectiveId = Dispatch.resolveRepeatIndexes( _container, result.get(), getActionPrefixedId(actionElement), repeatIndexes ); return _containingDocument.getControlByEffectiveId(effectiveId); } } } public String getActionPrefixedId(Element actionElement) { return _container.getFullPrefix() + getActionStaticId(actionElement); } public String getActionStaticId(Element actionElement) { final String staticId = XFormsUtils.getElementId(actionElement); assert staticId != null; return staticId; } public Scope getActionScope(Element actionElement) { return _container.getPartAnalysis().scopeForPrefixedId(getActionPrefixedId(actionElement)); } /** * Search a model given a static id and/or the current action element. * * @param actionElement current action element * @param modelStaticId static id of the model searched, or null if current model * @return model */ public XFormsModel resolveModel(Element actionElement, String modelStaticId) { final XFormsModel model; if (modelStaticId != null) { // Id is specified, resolve the effective object final XFormsObject o = resolveObject(actionElement, modelStaticId); if (!(o instanceof XFormsModel)) throw new ValidationException("Invalid model id: " + modelStaticId, (LocationData) actionElement.getData()); model = (XFormsModel) o; } else { // Id is not specified final Option<XFormsModel> modelOpt = _actionXPathContext.getCurrentBindingContext().modelOpt(); model = modelOpt.isDefined() ? modelOpt.get() : null; } if (model == null) throw new ValidationException("Invalid model id: " + modelStaticId, (LocationData) actionElement.getData()); return model; } public boolean isDeferredUpdates(Element actionElement) { final BindingContext bindingContext = _actionXPathContext.getCurrentBindingContext(); final boolean deferredUpdates; if (bindingContext.singleItemOpt().isDefined()) { deferredUpdates = ! "false".equals(resolveAVT(actionElement, XFormsConstants.XXFORMS_DEFERRED_UPDATES_QNAME)); } else { // TODO: Presence of context is not the right way to decide whether to evaluate AVTs or not deferredUpdates = true; } return deferredUpdates; } }