org.orbeon.oxf.xforms.XFormsContextStack.java Source code

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.xforms.XFormsContextStack.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.xforms;

import org.dom4j.Element;
import org.orbeon.oxf.common.OXFException;
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.analysis.ElementAnalysis;
import org.orbeon.oxf.xforms.analysis.VariableAnalysisTrait;
import org.orbeon.oxf.xforms.control.XFormsControlFactory;
import org.orbeon.oxf.xforms.function.XFormsFunction;
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.TransformerUtils;
import org.orbeon.oxf.xml.dom4j.ExtendedLocationData;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.saxon.om.Item;
import org.orbeon.saxon.om.NodeInfo;
import org.orbeon.saxon.om.ValueRepresentation;
import org.orbeon.saxon.tinytree.TinyBuilder;
import org.xml.sax.SAXException;

import javax.xml.transform.sax.TransformerHandler;
import java.util.*;

/**
 * Handle a stack of XPath evaluation context information. This is used by controls (with one stack rooted at each
 * XBLContainer), models, and actions.
 *
 * TODO: This has to go, and instead we will just use BindingContext.
 */
public class XFormsContextStack {

    private static final NodeInfo DUMMY_CONTEXT;
    static {
        try {
            final TinyBuilder treeBuilder = new TinyBuilder();
            final TransformerHandler identity = TransformerUtils
                    .getIdentityTransformerHandler(XPathCache.getGlobalConfiguration());
            identity.setResult(treeBuilder);

            identity.startDocument();
            identity.endDocument();

            DUMMY_CONTEXT = treeBuilder.getCurrentRoot();
        } catch (SAXException e) {
            throw new OXFException(e);
        }
    }

    // If there is no XPath context defined at the root (in the case there is no default XForms model/instance
    // available), we should use an empty context. However, currently for non-relevance in particular we must not run
    // expressions with an empty context. To allow running expressions at the root of a container without models, we
    // create instead a context with an empty document node instead. This way there is a context for evaluation. In the
    // future, we should allow running expressions with no context, possibly after statically checking that they do not
    // depend on the context, as well as prevent evaluations within non-relevant content by other means.
    //    final List<Item> DEFAULT_CONTEXT = XFormsConstants.EMPTY_ITEM_LIST;
    private static final List<Item> DEFAULT_CONTEXT = Collections.singletonList((Item) DUMMY_CONTEXT);

    private final boolean keepLocationData = XFormsProperties.isKeepLocation();

    public final XBLContainer container;
    public final XFormsContainingDocument containingDocument;

    private XFormsFunction.Context functionContext;
    private BindingContext parentBindingContext;

    private BindingContext head = null;

    // Constructor for XBLContainer
    public XFormsContextStack(XBLContainer container) {
        this.container = container;
        this.containingDocument = this.container.getContainingDocument();
        this.functionContext = new XFormsFunction.Context(container, this);
    }

    // Constructor for XFormsActionInterpreter
    public XFormsContextStack(XBLContainer container, BindingContext parentBindingContext) {
        this.container = container;
        this.containingDocument = this.container.getContainingDocument();
        this.functionContext = new XFormsFunction.Context(container, this);
        this.parentBindingContext = parentBindingContext;

        // Push a copy of the parent binding
        this.head = pushCopy(parentBindingContext);
    }

    // Push a copy of the current binding
    public BindingContext pushCopy() {
        return pushCopy(this.head);
    }

    private BindingContext pushCopy(BindingContext parent) {
        this.head = new BindingContext(parent, parent.model(), parent.bind(), parent.nodeset(), parent.position(),
                parent.elementId(), false, parent.controlElement(), parent.locationData(), false,
                parent.contextItem(), parent.scope());

        return this.head;
    }

    // Constructor for XFormsModel
    public XFormsContextStack(XFormsModel containingModel) {
        this.container = containingModel.container();
        this.containingDocument = this.container.getContainingDocument();
        this.functionContext = new XFormsFunction.Context(containingModel, this);
    }

    // For XBL/xxf:dynamic
    public void setParentBindingContext(BindingContext parentBindingContext) {
        this.parentBindingContext = parentBindingContext;
    }

    public XFormsFunction.Context getFunctionContext(String sourceEffectiveId) {
        functionContext.setSourceEffectiveId(sourceEffectiveId);
        functionContext.setSourceElement(this.head.controlElement());
        functionContext.setModel(this.head.model());
        return functionContext;
    }

    public void returnFunctionContext() {
        functionContext.setSourceEffectiveId(null);
        functionContext.setSourceElement(null);
    }

    /**
     * Reset the binding context to the root of the first model's first instance, or to the parent binding context.
     */
    public BindingContext resetBindingContext() {
        // Reset to default model (can be null)
        resetBindingContext(container.getDefaultModel());
        return this.head;
    }

    /**
     * Reset the binding context to the root of the given model's first instance.
     */
    public void resetBindingContext(XFormsModel model) {

        if (model != null && model.getDefaultInstance() != null) {
            // Push the default context if there is a model with an instance
            final Item defaultNode = model.getDefaultInstance().rootElement();
            final List<Item> defaultNodeset = Arrays.asList(defaultNode);
            this.head = new BindingContext(parentBindingContext, model, null, defaultNodeset, 1, null, true, null,
                    model.getDefaultInstance().getLocationData(), false, defaultNode, container.innerScope());
        } else {
            // Push empty context
            this.head = defaultContext(parentBindingContext, container, model);
        }

        // Add model variables for default model
        if (model != null)
            model.setTopLevelVariables(evaluateModelVariables(model));
    }

    /**
     * Return an empty context for the given model (which can be null).
     */
    public static BindingContext defaultContext(BindingContext parentBindingContext, XBLContainer container,
            XFormsModel model) {
        final List<Item> defaultContext = DEFAULT_CONTEXT;
        return new BindingContext(parentBindingContext, model, null, defaultContext, defaultContext.size(), null,
                true, null, (model != null) ? model.getLocationData() : null, false, null, container.innerScope());
    }

    // NOTE: This only scopes top-level model variables, but not binds-as-variables.
    private Map<String, ValueRepresentation> evaluateModelVariables(XFormsModel model) {
        // TODO: Check dirty flag to prevent needless re-evaluation
        // All variables in the model are in scope for the nested binds and actions.
        final List<VariableAnalysisTrait> variables = model.getStaticModel().jVariablesSeq();
        if (!variables.isEmpty()) {

            final Map<String, ValueRepresentation> variableInfos = new HashMap<String, ValueRepresentation>();

            for (final VariableAnalysisTrait variable : variables)
                variableInfos.put(variable.name(), scopeVariable(variable, model.getEffectiveId(), true).value());

            final IndentedLogger indentedLogger = containingDocument
                    .getIndentedLogger(XFormsModel.LOGGING_CATEGORY);
            if (indentedLogger.isDebugEnabled())
                indentedLogger.logDebug("", "evaluated model variables", "count",
                        Integer.toString(variableInfos.size()));

            // Remove extra bindings added and set all variables on the current binding context so that things are cleaner
            for (int i = 0; i < variableInfos.size(); i++)
                popBinding();

            return variableInfos;
        } else
            return Collections.emptyMap();
    }

    public VariableNameValue scopeVariable(VariableAnalysisTrait staticVariable, String sourceEffectiveId,
            boolean handleNonFatal) {

        // Create variable object
        final Variable variable = new Variable(staticVariable, containingDocument);

        // Find variable scope
        final Scope newScope = ((ElementAnalysis) staticVariable).scope();

        // Push the variable on the context stack. Note that we do as if each variable was a "parent" of the
        // following controls and variables.

        // NOTE: The value is computed immediately. We should use Expression objects and do lazy evaluation
        // in the future.

        // NOTE: We used to simply add variables to the current bindingContext, but this could cause issues
        // because getVariableValue() can itself use variables declared previously. This would work at first,
        // but because BindingContext caches variables in scope, after a first request for in-scope variables,
        // further variables values could not be added. The method below temporarily adds more elements on the
        // stack but it is safer.
        getFunctionContext(sourceEffectiveId);
        this.head = this.head.pushVariable((ElementAnalysis) staticVariable, variable.getVariableName(),
                variable.getVariableValue(this, sourceEffectiveId, true, handleNonFatal), newScope);
        returnFunctionContext();

        return this.head.variable().get();
    }

    public BindingContext setBinding(BindingContext bindingContext) {
        this.head = bindingContext;
        return this.head;
    }

    /**
     * Push an element containing either single-node or nodeset binding attributes.
     *
     * @param bindingElement    current element containing node binding attributes
     * @param sourceEffectiveId effective id of source control for id resolution of models and binds
     * @param scope             XBL scope
     */
    public void pushBinding(Element bindingElement, String sourceEffectiveId, Scope scope) {
        pushBinding(bindingElement, sourceEffectiveId, scope, true);
    }

    public void pushBinding(Element bindingElement, String sourceEffectiveId, Scope scope, boolean handleNonFatal) {
        // TODO: move away from element and use static analysis information
        final String ref = bindingElement.attributeValue(XFormsConstants.REF_QNAME);
        final String context = bindingElement.attributeValue(XFormsConstants.CONTEXT_QNAME);
        final String nodeset = bindingElement.attributeValue(XFormsConstants.NODESET_QNAME);
        final String model = bindingElement.attributeValue(XFormsConstants.MODEL_QNAME);
        final String bind = bindingElement.attributeValue(XFormsConstants.BIND_QNAME);

        final NamespaceMapping bindingElementNamespaceMapping = container.getNamespaceMappings(bindingElement);
        pushBinding(ref, context, nodeset, model, bind, bindingElement, bindingElementNamespaceMapping,
                sourceEffectiveId, scope, handleNonFatal);
    }

    private BindingContext getBindingContext(Scope scope) {
        BindingContext bindingContext = this.head;
        while (bindingContext.scope() != scope) {
            bindingContext = bindingContext.parent();
            // There must be a matching scope down the line
            assert bindingContext != null;
        }
        return bindingContext;
    }

    public void pushBinding(String ref, String context, String nodeset, String modelId, String bindId,
            Element bindingElement, NamespaceMapping bindingElementNamespaceMapping, String sourceEffectiveId,
            Scope scope, boolean handleNonFatal) {

        assert scope != null;

        final LocationData locationData;
        {
            if (keepLocationData)
                locationData = (bindingElement == null) ? container.getLocationData()
                        : new ExtendedLocationData((LocationData) bindingElement.getData(),
                                "pushing XForms control binding", bindingElement);
            else
                locationData = null;
        }

        try {
            // Handle scope
            // The new binding evaluates against a base binding context which must be in the same scope
            final BindingContext baseBindingContext = getBindingContext(scope);

            // Set context
            final String tempSourceEffectiveId = functionContext.sourceEffectiveId();
            if (sourceEffectiveId != null) {
                functionContext.setSourceEffectiveId(sourceEffectiveId);
            }

            // Handle model
            final XFormsModel newModel;
            final boolean isNewModel;
            if (modelId != null) {
                final XBLContainer resolutionScopeContainer = container.findScopeRoot(scope);
                final XFormsObject o = resolutionScopeContainer.resolveObjectById(sourceEffectiveId, modelId, null);
                if (!(o instanceof XFormsModel)) {
                    // Invalid model id

                    // NOTE: We used to dispatch xforms-binding-exception, but we want to be able to recover
                    if (!handleNonFatal)
                        throw new ValidationException("Reference to non-existing model id: " + modelId,
                                locationData);

                    // Default to not changing the model
                    newModel = baseBindingContext.model();
                    isNewModel = false;
                } else {
                    newModel = (XFormsModel) o;
                    isNewModel = newModel != baseBindingContext.model();// don't say it's a new model unless it has really changed
                }
            } else {
                newModel = baseBindingContext.model();
                isNewModel = false;
            }

            // Handle nodeset
            final boolean isNewBind;
            final RuntimeBind bind;
            final int newPosition;
            final List<Item> newNodeset;
            final boolean hasOverriddenContext;
            final Item contextItem;
            {
                if (bindId != null) {
                    // Resolve the bind id to a nodeset

                    // NOTE: For now, only the top-level models in a resolution scope are considered
                    final XBLContainer resolutionScopeContainer = container.findScopeRoot(scope);
                    final XFormsObject o = resolutionScopeContainer.resolveObjectById(sourceEffectiveId, bindId,
                            baseBindingContext.getSingleItem());
                    if (o == null && resolutionScopeContainer.containsBind(bindId)) {
                        // The bind attribute was valid for this scope, but no runtime object was found for the bind
                        // This can happen e.g. if a nested bind is within a bind with an empty nodeset

                        bind = null;
                        newNodeset = XFormsConstants.EMPTY_ITEM_LIST;
                        hasOverriddenContext = false;
                        contextItem = null;
                        isNewBind = true;
                        newPosition = 0;
                    } else if (!(o instanceof RuntimeBind)) {
                        // The bind attribute did not resolve to a bind

                        // NOTE: We used to dispatch xforms-binding-exception, but we want to be able to recover
                        if (!handleNonFatal)
                            throw new ValidationException("Reference to non-existing bind id: " + bindId,
                                    locationData);

                        // Default to an empty binding
                        bind = null;
                        newNodeset = XFormsConstants.EMPTY_ITEM_LIST;
                        hasOverriddenContext = false;
                        contextItem = null;
                        isNewBind = true;
                        newPosition = 0;
                    } else {
                        bind = (RuntimeBind) o;
                        newNodeset = bind.nodeset;
                        hasOverriddenContext = false;
                        contextItem = baseBindingContext.getSingleItem();
                        isNewBind = true;
                        newPosition = Math.min(newNodeset.size(), 1);
                    }
                } else if (ref != null || nodeset != null) {

                    bind = null;

                    // Check whether there is an optional context (XForms 1.1, likely generalized in XForms 1.2)
                    final BindingContext evaluationContextBinding;
                    if (context != null) {
                        // Push model and context
                        pushTemporaryContext(this.head, baseBindingContext, baseBindingContext.getSingleItem());// provide context information for the context() function
                        pushBinding(null, null, context, modelId, null, null, bindingElementNamespaceMapping,
                                sourceEffectiveId, scope, handleNonFatal);
                        hasOverriddenContext = true;
                        final BindingContext newBindingContext = this.head;
                        contextItem = newBindingContext.getSingleItem();
                        evaluationContextBinding = newBindingContext;
                    } else if (isNewModel) {
                        // Push model only
                        pushBinding(null, null, null, modelId, null, null, bindingElementNamespaceMapping,
                                sourceEffectiveId, scope, handleNonFatal);
                        hasOverriddenContext = false;
                        final BindingContext newBindingContext = this.head;
                        contextItem = newBindingContext.getSingleItem();
                        evaluationContextBinding = newBindingContext;
                    } else {
                        hasOverriddenContext = false;
                        contextItem = baseBindingContext.getSingleItem();
                        evaluationContextBinding = baseBindingContext;
                    }

                    if (false) {
                        // NOTE: This is an attempt at allowing evaluating a binding even if no context is present.
                        // But this doesn't work properly. E.g.:
                        //
                        // <xf:group ref="()">
                        //   <xf:input ref="."/>
                        //
                        // Above must end up with an empty binding for xf:input, while:
                        //
                        // <xf:group ref="()">
                        //   <xf:input ref="instance('foobar')"/>
                        //
                        // Above must end up with a non-empty binding IF it was to be evaluated.
                        //
                        // Now the second condition above should not happen anyway, because the content of the group
                        // is non-relevant anyway. But we do have cases now where this happens, so we can't enable
                        // the code below naively.
                        //
                        // We could enable it if we knew statically that the expression did not depend on the
                        // context though, but right now we don't.

                        final boolean isDefaultContext;
                        final List<Item> evaluationNodeset;
                        final int evaluationPosition;
                        if (evaluationContextBinding.getNodeset().size() > 0) {
                            isDefaultContext = false;
                            evaluationNodeset = evaluationContextBinding.getNodeset();
                            evaluationPosition = evaluationContextBinding.getPosition();
                        } else {
                            isDefaultContext = true;
                            evaluationNodeset = DEFAULT_CONTEXT;
                            evaluationPosition = 1;
                        }

                        if (!isDefaultContext) {
                            // Provide context information for the context() function
                            pushTemporaryContext(this.head, evaluationContextBinding,
                                    evaluationContextBinding.getSingleItem());
                        }

                        // Use updated binding context to set model
                        functionContext.setModel(evaluationContextBinding.model());

                        List<Item> result;
                        try {
                            result = XPathCache.evaluateKeepItems(evaluationNodeset, evaluationPosition,
                                    ref != null ? ref : nodeset, bindingElementNamespaceMapping,
                                    evaluationContextBinding.getInScopeVariables(),
                                    XFormsContainingDocument.getFunctionLibrary(), functionContext, null,
                                    locationData, containingDocument.getRequestStats().getReporter());
                        } catch (Exception e) {
                            if (handleNonFatal) {
                                XFormsError.handleNonFatalXPathError(container, e);
                                result = XFormsConstants.EMPTY_ITEM_LIST;
                            } else {
                                throw e;
                            }
                        }
                        newNodeset = result;

                        if (!isDefaultContext) {
                            popBinding();
                        }
                    } else {
                        if (evaluationContextBinding.getNodeset().size() > 0) {
                            // Evaluate new XPath in context if the current context is not empty

                            // TODO: in the future, we should allow null context for expressions that do not depend on the context
                            // NOTE: We prevent evaluation if the context was empty. However there are cases where this
                            // should be allowed, if the expression does not depend on the context. Ideally, we would know
                            // statically whether an expression depends on the context or not, and take separate action if
                            // that's the case. Currently, such an expression will produce an XPathException.

                            // It might be the case that when we implement non-evaluation of relevant subtrees, this won't
                            // be an issue anymore, and we can simply allow evaluation of such expressions. Otherwise,
                            // static analysis of expressions might provide enough information to handle the two situations.

                            pushTemporaryContext(this.head, evaluationContextBinding,
                                    evaluationContextBinding.getSingleItem());// provide context information for the context() function

                            // Use updated binding context to set model
                            functionContext.setModel(evaluationContextBinding.model());

                            List<Item> result;
                            try {
                                result = XPathCache.evaluateKeepItems(evaluationContextBinding.getNodeset(),
                                        evaluationContextBinding.getPosition(), ref != null ? ref : nodeset,
                                        bindingElementNamespaceMapping,
                                        evaluationContextBinding.getInScopeVariables(),
                                        XFormsContainingDocument.getFunctionLibrary(), functionContext, null,
                                        locationData, containingDocument.getRequestStats().getReporter());
                            } catch (Exception e) {
                                if (handleNonFatal) {
                                    XFormsError.handleNonFatalXPathError(container, e);
                                    result = XFormsConstants.EMPTY_ITEM_LIST;
                                } else {
                                    throw e;
                                }

                            }
                            newNodeset = result;

                            popBinding();
                        } else {
                            // Otherwise we consider we can't evaluate
                            newNodeset = XFormsConstants.EMPTY_ITEM_LIST;
                        }
                    }

                    // Restore optional context
                    if (context != null || isNewModel) {
                        popBinding();
                        if (context != null)
                            popBinding();
                    }
                    isNewBind = true;
                    newPosition = 1;
                } else if (isNewModel && context == null) {
                    // Only the model has changed

                    bind = null;

                    final BindingContext modelBindingContext = getCurrentBindingContextForModel(newModel);
                    if (modelBindingContext != null) {
                        newNodeset = modelBindingContext.getNodeset();
                        newPosition = modelBindingContext.getPosition();
                    } else {
                        newNodeset = getCurrentNodeset(newModel);
                        newPosition = 1;
                    }

                    hasOverriddenContext = false;
                    contextItem = baseBindingContext.getSingleItem();
                    isNewBind = false;

                } else if (context != null) {

                    bind = null;

                    // Only the context has changed, and possibly the model
                    pushBinding(null, null, context, modelId, null, null, bindingElementNamespaceMapping,
                            sourceEffectiveId, scope, handleNonFatal);
                    {
                        newNodeset = getCurrentNodeset();
                        newPosition = getCurrentPosition();
                        isNewBind = false;
                        hasOverriddenContext = true;
                        contextItem = getCurrentSingleItem();
                    }
                    popBinding();

                } else {
                    // No change to anything
                    bind = null;
                    isNewBind = false;
                    newNodeset = baseBindingContext.getNodeset();
                    newPosition = baseBindingContext.getPosition();

                    // We set a new context item as the context into which other attributes must be evaluated. E.g.:
                    //
                    // <xf:select1 ref="type">
                    //   <xf:action ev:event="xforms-value-changed" if="context() = 'foobar'">
                    //
                    // In this case, you expect context() to be updated as follows.
                    //
                    hasOverriddenContext = false;
                    contextItem = baseBindingContext.getSingleItem();
                }
            }

            // Push new context
            final String bindingElementId = (bindingElement == null) ? null
                    : XFormsUtils.getElementId(bindingElement);
            this.head = new BindingContext(this.head, newModel, bind, newNodeset, newPosition, bindingElementId,
                    isNewBind, bindingElement, locationData, hasOverriddenContext, contextItem, scope);

            // Restore context
            if (sourceEffectiveId != null) {
                functionContext.setSourceEffectiveId(tempSourceEffectiveId);
            }
        } catch (Exception e) {
            if (bindingElement != null) {
                throw OrbeonLocationException.wrapException(e,
                        new ExtendedLocationData(locationData, "evaluating binding expression", bindingElement));
            } else {
                throw OrbeonLocationException.wrapException(e,
                        new ExtendedLocationData(locationData, "evaluating binding expression", bindingElement,
                                new String[] { "ref", ref, "context", context, "nodeset", nodeset, "modelId",
                                        modelId, "bindId", bindId }));
            }
        }
    }

    private void pushTemporaryContext(BindingContext parent, BindingContext base, Item contextItem) {
        this.head = new BindingContext(parent, base.model(), null, base.getNodeset(), base.getPosition(),
                base.elementId(), false, base.getControlElement(), base.locationData(), false, contextItem,
                base.scope());
    }

    /**
     * Push an iteration of the current node-set. Used for example by xf:repeat, xf:bind, iterate.
     *
     * @param currentPosition   1-based iteration index
     */
    public BindingContext pushIteration(int currentPosition) {
        final BindingContext currentBindingContext = this.head;
        final List<Item> currentNodeset = currentBindingContext.getNodeset();

        // Set a new context item, although the context() function is never called on the iteration itself
        final Item newContextItem;
        if (currentNodeset.size() == 0)
            newContextItem = null;
        else
            newContextItem = currentNodeset.get(currentPosition - 1);

        this.head = new BindingContext(currentBindingContext, currentBindingContext.model(), null, currentNodeset,
                currentPosition, currentBindingContext.elementId(), true, null,
                currentBindingContext.locationData(), false, newContextItem, currentBindingContext.scope());

        return this.head;
    }

    public BindingContext getCurrentBindingContext() {
        return head;
    }

    public BindingContext popBinding() {
        if (this.head.parent() == null)
            throw new OXFException("Attempt to clear context stack.");
        final BindingContext popped = this.head;
        this.head = this.head.parent();
        return popped;
    }

    /**
     * Get the current node-set binding for the given model.
     */
    public BindingContext getCurrentBindingContextForModel(XFormsModel model) {

        BindingContext currentBindingContext = this.head;
        while (currentBindingContext != null) {
            if (model == currentBindingContext.model())
                return currentBindingContext;
            currentBindingContext = currentBindingContext.parent();
        }

        return null;
    }

    /**
     * Get the current node-set binding for the given model.
     */
    public List<Item> getCurrentNodeset(XFormsModel model) {

        final BindingContext bindingContext = getCurrentBindingContextForModel(model);

        // If a context exists, return its node-set
        if (bindingContext != null)
            return bindingContext.getNodeset();

        // If there is no default instance, return an empty node-set
        final XFormsInstance defaultInstance = model.getDefaultInstance();
        if (defaultInstance == null)
            return XFormsConstants.EMPTY_ITEM_LIST;

        // If not found, return the document element of the model's default instance
        try {
            return Collections.singletonList((Item) defaultInstance.rootElement());
        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    /**
     * Get the current single item, if any.
     */
    public Item getCurrentSingleItem() {
        return this.head.getSingleItem();
    }

    /**
     * Get the context item, whether in-scope or overridden.
     */
    public Item getContextItem() {
        return this.head.contextItem();
    }

    /**
     * Return whether there is an overridden context, whether empty or not.
     */
    public boolean hasOverriddenContext() {
        return this.head.hasOverriddenContext();
    }

    /**
     * Get the current nodeset binding, if any.
     */
    public List<Item> getCurrentNodeset() {
        return this.head.getNodeset();
    }

    /**
     * Get the current position in current nodeset binding.
     */
    public int getCurrentPosition() {
        return this.head.getPosition();
    }

    /**
     * Return a Map of the current variables in scope.
     *
     * @return  Map<String, List> of variable name to value
     */
    public Map<String, ValueRepresentation> getCurrentVariables() {
        return this.head.getInScopeVariables();
    }

    public boolean isRepeatIterationBindingContext(BindingContext bindingContext) {
        // First, we need a parent
        final BindingContext parent = bindingContext.parent();
        if (parent == null)
            return false;
        // We don't have a bound element, but the parent is bound to xf:repeat
        final Element bindingElement = bindingContext.getControlElement();
        final Element parentBindingElement = parent.getControlElement();
        return (bindingElement == null) && (parentBindingElement != null)
                && parentBindingElement.getName().equals("repeat");
    }

    private boolean isRepeatBindingContext(BindingContext bindingContext) {
        final Element bindingElement = bindingContext.getControlElement();
        return bindingElement != null && bindingElement.getName().equals("repeat");
    }

    /**
     * Return the closest enclosing repeat id.
     *
     * @return  repeat id, throw if not found
     */
    public String getEnclosingRepeatId() {
        BindingContext currentBindingContext = this.head;
        do {
            // Handle case where we are within a repeat iteration, as well as case where we are directly within the
            // repeat container object.
            if (isRepeatIterationBindingContext(currentBindingContext)
                    || isRepeatBindingContext(currentBindingContext)) {
                // Found binding context for relevant repeat iteration
                return currentBindingContext.parent().elementId();
            }
            currentBindingContext = currentBindingContext.parent();
        } while (currentBindingContext != null);
        // It is required that there is an enclosing xf:repeat
        throw new ValidationException("Enclosing xf:repeat not found.", this.head.locationData());
    }

    /**
     * Obtain the single-node binding for an enclosing xf:group, xf:repeat, or xf:switch. It takes one
     * mandatory string parameter containing the id of an enclosing grouping XForms control. For xf:repeat, the
     * context returned is the context of the current iteration.
     *
     * @param contextId  enclosing context id
     * @return           the item
     */
    public Item getContextForId(String contextId) {
        BindingContext currentBindingContext = this.head;
        do {
            final Element bindingElement = currentBindingContext.getControlElement();
            if (bindingElement != null && XFormsControlFactory.isContainerControl(bindingElement.getNamespaceURI(),
                    bindingElement.getName())) {
                // We are a grouping control
                final String elementId = currentBindingContext.elementId();
                if (contextId.equals(elementId)) {
                    // Found matching binding context for regular grouping control
                    return currentBindingContext.getSingleItem();
                }
            } else if (bindingElement == null) {
                // We a likely repeat iteration
                if (isRepeatIterationBindingContext(currentBindingContext)
                        && currentBindingContext.parent().elementId().equals(contextId)) {
                    // Found matching repeat iteration
                    return currentBindingContext.getSingleItem();
                }
            }
            currentBindingContext = currentBindingContext.parent();
        } while (currentBindingContext != null);
        // It is required that there is an enclosing container control
        throw new ValidationException("No enclosing container XForms control found for id: " + contextId,
                this.head.locationData());
    }

    /**
     * Get the current node-set for the given repeat id.
     *
     * @param repeatId  existing repeat id
     * @return          node-set
     */
    public List getRepeatNodeset(String repeatId) {
        BindingContext currentBindingContext = this.head;
        do {
            final Element bindingElement = currentBindingContext.getControlElement();
            final String elementId = currentBindingContext.elementId();
            if (repeatId.equals(elementId) && bindingElement != null && bindingElement.getName().equals("repeat")) {
                // Found repeat, return associated node-set
                return currentBindingContext.getNodeset();
            }
            currentBindingContext = currentBindingContext.parent();
        } while (currentBindingContext != null);
        // It is required that there is an enclosing xf:repeat
        throw new ValidationException("No enclosing xf:repeat found for id: " + repeatId, this.head.locationData());
    }

    /**
     * Return the current model for the current nodeset binding.
     */
    public XFormsModel getCurrentModel() {
        return this.head.model();
    }
}