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

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.xforms.XFormsModelBinds.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.dom4j.QName;
import org.orbeon.oxf.common.OrbeonLocationException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.util.IndentedLogger;
import org.orbeon.oxf.util.XPath;
import org.orbeon.oxf.xforms.analysis.XPathDependencies;
import org.orbeon.oxf.xforms.analysis.model.Model;
import org.orbeon.oxf.xforms.analysis.model.StaticBind;
import org.orbeon.oxf.xforms.model.DataModel;
import org.orbeon.oxf.xml.NamespaceMapping;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.XMLUtils;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.ExtendedLocationData;
import org.orbeon.saxon.expr.XPathContext;
import org.orbeon.saxon.expr.XPathContextMajor;
import org.orbeon.saxon.om.*;
import org.orbeon.saxon.sxpath.IndependentContext;
import org.orbeon.saxon.sxpath.XPathEvaluator;
import org.orbeon.saxon.trans.XPathException;
import org.orbeon.saxon.type.BuiltInAtomicType;
import org.orbeon.saxon.type.BuiltInType;
import org.orbeon.saxon.type.ConversionResult;
import org.orbeon.saxon.type.ValidationFailure;
import org.orbeon.saxon.value.BooleanValue;
import org.orbeon.saxon.value.QNameValue;
import org.orbeon.saxon.value.StringValue;
import org.orbeon.scaxon.XML;
import org.w3c.dom.Node;

import java.util.*;

/**
 * Represent a given model's binds.
 */
public class XFormsModelBinds extends XFormsModelBindsBase {

    public final XFormsModel model; // model to which we belong
    private final Model staticModel;

    private final IndentedLogger indentedLogger;
    public final XFormsContainingDocument containingDocument; // current containing document
    private final XPathDependencies dependencies;

    private List<RuntimeBind> topLevelBinds = new ArrayList<RuntimeBind>();

    private XFormsModelSchemaValidator xformsValidator; // validator for standard XForms schema types

    private boolean isFirstCalculate; // whether this is the first recalculate for the associated XForms model
    private boolean isFirstRebuild; // whether this is the first rebuild for the associated XForms model

    /**
     * Create an instance of XFormsModelBinds if the given model has xf:bind elements.
     *
     * @param model XFormsModel
     * @return      XFormsModelBinds or null if the model doesn't have xf:bind elements
     */
    public static XFormsModelBinds create(XFormsModel model) {
        return model.getStaticModel().hasBinds() ? new XFormsModelBinds(model) : null;
    }

    private XFormsModelBinds(XFormsModel model) {
        super(model);
        this.model = model;

        this.indentedLogger = model.getIndentedLogger();
        this.containingDocument = model.containingDocument;
        this.dependencies = this.containingDocument.getXPathDependencies();

        this.staticModel = model.getStaticModel();

        // For the lifecycle of an XForms document, new XFormsModelBinds() may be created multiple times, e.g. if the
        // state is deserialized, but we know that new XFormsModelBinds() will occur only once during document
        // initialization. So the assignation below is ok.
        this.isFirstCalculate = this.isFirstRebuild = containingDocument.isInitializing();
    }

    public void resetFirstCalculate() {
        this.isFirstCalculate = true;
    }

    /**
     * Rebuild all binds, computing all bind nodesets (but not computing the MIPs)
     */
    public void rebuild() {

        if (indentedLogger.isDebugEnabled())
            indentedLogger.startHandleOperation("model", "performing rebuild", "model id", model.getEffectiveId());

        // Reset everything
        // NOTE: Assume that model.getContextStack().resetBindingContext(model) was called
        topLevelBinds.clear();
        singleNodeContextBinds().clear();
        iterationsForContextNodeInfo().clear();

        // Clear all instances that might have InstanceData
        // Only need to do this after the first rebuild
        if (!isFirstRebuild) {
            for (final XFormsInstance instance : model.getInstances()) {
                // Only clear instances that are impacted by xf:bind/(@ref|@nodeset), assuming we were able to figure out the dependencies
                // The reason is that clearing this state can take quite some time
                final boolean instanceMightBeSchemaValidated = model.hasSchema() && instance.isSchemaValidation();
                final boolean instanceMightHaveMips = dependencies.hasAnyCalculationBind(staticModel,
                        instance.getPrefixedId())
                        || dependencies.hasAnyValidationBind(staticModel, instance.getPrefixedId());

                if (instanceMightBeSchemaValidated || instanceMightHaveMips) {
                    DataModel.visitElementJava(instance.rootElement(), new DataModel.NodeVisitor() {
                        public void visit(NodeInfo nodeInfo) {
                            InstanceData.clearState(nodeInfo);
                        }
                    });
                }
            }
        }

        // Iterate through all top-level bind elements to create new bind tree
        // TODO: In the future, XPath dependencies must allow for partial rebuild of the tree as is the case with controls
        // Even before that, the bind tree could be modified more dynamically as is the case with controls
        for (final StaticBind staticBind : staticModel.topLevelBindsJava())
            topLevelBinds.add(new RuntimeBind(XFormsModelBinds.this, staticBind, true)); // remember as top-level bind

        isFirstRebuild = false;

        if (indentedLogger.isDebugEnabled())
            indentedLogger.endHandleOperation();
    }

    /**
     * Apply calculate binds.
     *
     * @param applyDefaults    whether to apply initial values (@xxf:default="...")
     */
    public void applyCalculateBinds(boolean applyDefaults) {

        if (!staticModel.hasCalculateComputedCustomBind()) {
            // We can skip this
            if (indentedLogger.isDebugEnabled())
                indentedLogger.logDebug("model", "skipping bind recalculate", "model id", model.getEffectiveId(),
                        "reason", "no recalculation binds");
        } else {
            // This model may have calculation binds

            if (indentedLogger.isDebugEnabled())
                indentedLogger.startHandleOperation("model", "performing bind recalculate", "model id",
                        model.getEffectiveId());
            {
                // 1. Evaluate initial values and calculate before the rest

                if (isFirstCalculate || applyDefaults) {
                    // Handle default values first
                    if (staticModel.hasDefaultValueBind())
                        iterateBinds(new BindRunner() {
                            public void applyBind(RuntimeBind bind, int position) {
                                if (bind.staticBind.getDefaultValue() != null && dependencies
                                        .requireModelMIPUpdate(staticModel, bind.staticBind, Model.DEFAULT(), null))
                                    handleDefaultValueBind(bind, position);
                            }
                        });
                    // This will be false from now on as we have done our first handling of calculate binds
                    isFirstCalculate = false;
                }

                // Handle calculations
                if (staticModel.hasCalculateBind())
                    iterateBinds(new BindRunner() {
                        public void applyBind(RuntimeBind bind, int position) {
                            if (bind.staticBind.getCalculate() != null && dependencies
                                    .requireModelMIPUpdate(staticModel, bind.staticBind, Model.CALCULATE(), null))
                                handleCalculateBind(bind, position);
                        }
                    });

                // 2. Update computed expression binds if requested
                applyComputedExpressionBinds();
            }

            if (indentedLogger.isDebugEnabled())
                indentedLogger.endHandleOperation();
        }
    }

    /**
     * Apply required, relevant and readonly binds.
     *
     */
    public void applyComputedExpressionBinds() {

        // Reset context stack just to re-evaluate the variables as instance values may have changed with @calculate
        model.resetAndEvaluateVariables();

        // Apply
        iterateBinds(new BindRunner() {
            public void applyBind(RuntimeBind bind, int position) {
                if (bind.staticBind.hasCalculateComputedMIPs() || bind.staticBind.hasCustomMIPs()) // don't bother if not
                    handleComputedExpressionBind(bind, position);
            }
        });
    }

    /**
     * Apply validation binds
     *
     * @param invalidInstances set filled with invalid instances if any
     */
    public void applyValidationBinds(final Set<String> invalidInstances) {

        if (!staticModel.hasValidateBind()) {
            // We can skip this
            if (indentedLogger.isDebugEnabled())
                indentedLogger.logDebug("model", "skipping bind revalidate", "model id", model.getEffectiveId(),
                        "reason", "no validation binds");
        } else {
            // This model may have validation binds

            // Reset context stack just to re-evaluate the variables
            model.resetAndEvaluateVariables();

            // 1. Validate based on type and requiredness
            if (staticModel.hasTypeBind() || staticModel.hasRequiredBind())
                iterateBinds(new BindRunner() {
                    public void applyBind(RuntimeBind bind, int position) {
                        if (bind.staticBind.dataTypeOrNull() != null || bind.staticBind.getRequired() != null) // don't bother if not
                            validateTypeAndRequired(bind, position, invalidInstances);
                    }
                });

            // 2. Validate constraints
            if (staticModel.hasConstraintBind())
                iterateBinds(new BindRunner() {
                    public void applyBind(RuntimeBind bind, int position) {
                        if (bind.staticBind.constraintsByLevel().nonEmpty()) // don't bother if not
                            validateConstraint(bind, position, invalidInstances);
                    }
                });
        }
    }

    public RuntimeBind resolveBind(String bindId, Item contextItem) {

        final RuntimeBind singleNodeContextBind = singleNodeContextBinds().get(bindId);
        if (singleNodeContextBind != null) {
            // This bind has a single-node context (incl. top-level bind), so ignore context item and just return the bind nodeset
            return singleNodeContextBind;
        } else {
            // Nested bind, context item will be used

            // This requires a context node, not just any item
            if (contextItem instanceof NodeInfo) {
                final List<RuntimeBind.BindIteration> iterationsForContextNode = iterationsForContextNodeInfo()
                        .get(contextItem);
                if (iterationsForContextNode != null) {
                    for (final RuntimeBind.BindIteration currentIteration : iterationsForContextNode) {
                        final RuntimeBind currentBind = currentIteration.getBind(bindId);
                        if (currentBind != null) {
                            // Found
                            return currentBind;
                        }
                    }
                }
            }
            // "From among the bind objects associated with the target bind element, if there exists a bind object
            // created with the same in-scope evaluation context node as the source object, then that bind object is the
            // desired target bind object. Otherwise, the IDREF resolution produced a null search result."
        }

        // Nothing found
        return null;
    }

    public Item evaluateBindByType(RuntimeBind bind, int position, QName mipType) throws XPathException {

        if (mipType.equals(XFormsConstants.RELEVANT_QNAME)) {
            // Relevant
            final Boolean relevant = evaluateRelevantMIP(bind, position);
            return (relevant != null) ? BooleanValue.get(relevant) : null;
        } else if (mipType.equals(XFormsConstants.READONLY_QNAME)) {
            // Readonly
            final Boolean readonly = evaluateReadonlyMIP(bind, position);
            return (readonly != null) ? BooleanValue.get(readonly) : null;
        } else if (mipType.equals(XFormsConstants.REQUIRED_QNAME)) {
            // Required
            final Boolean required = evaluateRequiredMIP(bind, position);
            return (required != null) ? BooleanValue.get(required) : null;
        } else if (mipType.equals(XFormsConstants.TYPE_QNAME)) {
            // Type
            final NamespaceMapping namespaceMapping = bind.staticBind.namespaceMapping();
            final QName type = bind.evaluateTypeQName(namespaceMapping.mapping);
            return (type != null)
                    ? new QNameValue(type.getNamespacePrefix(), type.getNamespaceURI(), type.getName(), null)
                    : null;
        } else if (mipType.equals(XFormsConstants.CONSTRAINT_QNAME)) {
            // Constraint
            // TODO: Add support for other constraint levels.
            if (bind.staticBind.constraintsByLevel().nonEmpty())
                return BooleanValue.get(failedConstraintMIPs(StaticBind.jErrorLevel(), bind, position).isEmpty());
            else
                return null;
        } else if (mipType.equals(XFormsConstants.CALCULATE_QNAME)) {
            // Calculate
            final String result = evaluateCalculateBind(bind, position);
            return (result != null) ? new StringValue(result) : null;
        } else if (mipType.equals(XFormsConstants.XXFORMS_DEFAULT_QNAME)) {
            // xxf:default
            final String result = evaluateXXFormsDefaultBind(bind, position);
            return (result != null) ? new StringValue(result) : null;
        } else {
            // Try custom MIPs
            final String result = evaluateCustomMIP(bind, Model.buildCustomMIPName(mipType.getQualifiedName()),
                    position);
            return (result != null) ? new StringValue(result) : null;
        }
    }

    // Iterate over all binds and for each one do the callback.
    private void iterateBinds(BindRunner bindRunner) {
        // Iterate over top-level binds
        for (final RuntimeBind currentBind : topLevelBinds) {
            try {
                currentBind.applyBinds(bindRunner);
            } catch (Exception e) {
                throw OrbeonLocationException.wrapException(e,
                        new ExtendedLocationData(currentBind.staticBind.locationData(), "evaluating XForms binds",
                                currentBind.staticBind.element()));
            }
        }
    }

    private String evaluateXXFormsDefaultBind(RuntimeBind bind, int position) {
        // Handle xxf:default MIP
        if (bind.staticBind.getDefaultValue() != null) {
            // Compute default value
            try {
                return evaluateStringExpression(bind.nodeset, position, bind.staticBind.getDefaultValue());
            } catch (Exception e) {
                handleMIPXPathException(e, bind, bind.staticBind.getDefaultValue(),
                        "evaluating XForms default bind");
                return null;
            }
        } else {
            return null;
        }
    }

    private void handleDefaultValueBind(RuntimeBind bind, int position) {

        final String stringResult = evaluateXXFormsDefaultBind(bind, position);
        if (stringResult != null) {
            // TODO: Detect if we have already handled this node and handle this error
            final NodeInfo currentNodeInfo = (NodeInfo) bind.nodeset.get(position - 1);
            DataModel.jSetValueIfChanged(containingDocument, model, bind.staticBind.locationData(), currentNodeInfo,
                    stringResult, "default", true, indentedLogger);
        }
    }

    public void handleCalculateBind(RuntimeBind bind, int position) {
        final String stringResult = evaluateCalculateBind(bind, position);
        if (stringResult != null) {
            // TODO: Detect if we have already handled this node and handle this error
            final NodeInfo currentNodeInfo = (NodeInfo) bind.nodeset.get(position - 1);
            DataModel.jSetValueIfChanged(containingDocument, model, bind.staticBind.locationData(), currentNodeInfo,
                    stringResult, "calculate", true, indentedLogger);
        }
    }

    public String evaluateCalculateBind(RuntimeBind bind, int position) {
        // Handle calculate MIP
        if (bind.staticBind.getCalculate() != null) {
            // Compute calculated value
            try {
                return evaluateStringExpression(bind.nodeset, position, bind.staticBind.getCalculate());
            } catch (Exception e) {
                handleMIPXPathException(e, bind, bind.staticBind.getCalculate(),
                        "evaluating XForms calculate bind");
                // Blank value so we don't have stale calculated values
                return "";
            }
        } else {
            return null;
        }
    }

    private void handleComputedExpressionBind(RuntimeBind bind, int position) {

        // Handle relevant, readonly, required, and custom MIPs
        if (bind.staticBind.getRelevant() != null
                && dependencies.requireModelMIPUpdate(staticModel, bind.staticBind, Model.RELEVANT(), null))
            evaluateAndSetRelevantMIP(bind, position);
        if (bind.staticBind.getReadonly() != null
                && dependencies.requireModelMIPUpdate(staticModel, bind.staticBind, Model.READONLY(), null)
                || bind.staticBind.getCalculate() != null)
            evaluateAndSetReadonlyMIP(bind, position);
        if (bind.staticBind.getRequired() != null
                && dependencies.requireModelMIPUpdate(staticModel, bind.staticBind, Model.REQUIRED(), null))
            evaluateAndSetRequiredMIP(bind, position);

        // TODO: optimize those as well
        evaluateAndSetCustomMIPs(bind, position);
    }

    private void evaluateAndSetRequiredMIP(RuntimeBind bind, int position) {
        final Boolean required = evaluateRequiredMIP(bind, position);
        if (required != null) {
            // Update node with MIP value
            bind.setRequired(position, required);
        }
    }

    private Boolean evaluateRequiredMIP(RuntimeBind bind, int position) {
        if (bind.staticBind.getRequired() != null) {
            // Evaluate "required" XPath expression on this node
            try {
                // Get MIP value
                return evaluateBooleanExpression(bind.nodeset, position, bind.staticBind.getRequired());
            } catch (Exception e) {
                handleMIPXPathException(e, bind, bind.staticBind.getRequired(), "evaluating XForms required bind");
                return !Model.DEFAULT_REQUIRED(); // https://github.com/orbeon/orbeon-forms/issues/835
            }
        } else {
            return null;
        }
    }

    private void evaluateAndSetReadonlyMIP(RuntimeBind bind, int position) {
        final Boolean readonly = evaluateReadonlyMIP(bind, position);
        if (readonly != null) {
            // Mark node
            bind.setReadonly(position, readonly);
        } else if (bind.staticBind.getCalculate() != null) {
            // The bind doesn't have a readonly attribute, but has a calculate: set readonly to true()
            bind.setReadonly(position, true);
        }
    }

    private Boolean evaluateReadonlyMIP(RuntimeBind bind, int position) {
        if (bind.staticBind.getReadonly() != null) {
            // The bind has a readonly attribute
            // Evaluate "readonly" XPath expression on this node
            try {
                return evaluateBooleanExpression(bind.nodeset, position, bind.staticBind.getReadonly());
            } catch (Exception e) {
                handleMIPXPathException(e, bind, bind.staticBind.getReadonly(), "evaluating XForms readonly bind");
                return !Model.DEFAULT_READONLY(); // https://github.com/orbeon/orbeon-forms/issues/835
            }
        } else {
            return null;
        }
    }

    private void evaluateAndSetRelevantMIP(RuntimeBind bind, int position) {
        final Boolean relevant = evaluateRelevantMIP(bind, position);
        if (relevant != null) {
            // Mark node
            bind.setRelevant(position, relevant);
        }
    }

    private Boolean evaluateRelevantMIP(RuntimeBind bind, int position) {
        if (bind.staticBind.getRelevant() != null) {
            // Evaluate "relevant" XPath expression on this node
            try {
                return evaluateBooleanExpression(bind.nodeset, position, bind.staticBind.getRelevant());
            } catch (Exception e) {
                handleMIPXPathException(e, bind, bind.staticBind.getRelevant(), "evaluating XForms relevant bind");
                return !Model.DEFAULT_RELEVANT(); // https://github.com/orbeon/orbeon-forms/issues/835
            }
        } else {
            return null;
        }
    }

    private void validateTypeAndRequired(RuntimeBind bind, int position, Set<String> invalidInstances) {

        assert bind.staticBind.dataTypeOrNull() != null || bind.staticBind.getRequired() != null;

        // Don't try to apply validity to a node if it has children nodes or if it's not a node
        // "The type model item property is not applied to instance nodes that contain child elements"
        final BindNode bindNode = bind.getBindNode(position);
        final NodeInfo currentNodeInfo = bindNode.node();
        if (currentNodeInfo == null || bindNode.hasChildrenElements())
            return;

        // NOTE: 2011-02-03: Decided to also apply this to required validation.
        // See: http://forge.ow2.org/tracker/index.php?func=detail&aid=315821&group_id=168&atid=350207

        // Current required value (computed during previous recalculate)
        final boolean isRequired = InstanceData.getRequired(currentNodeInfo);

        // 1. Check type validity

        // Type MIP @type attribute is special:
        //
        // - it is not an XPath expression
        // - but because type validation can be expensive, we want to optimize that if we can
        // - so requireModelMIPUpdate(Model.TYPE) actually means "do we need to update type validity"
        //
        // xxf:xml and xxf:xpath2 also depend on requiredness, which is probably not a good idea. To handle
        // this condition (partially), if the same bind has @type and @required, we also reevaluate type validity if
        // requiredness has changed. Ideally:
        //
        // - we would not depend on requiredness
        // - but if we did, we should handle also the case where another bind is setting requiredness on the node
        //
        final boolean typeValidity;
        if (bind.typeQName != null) {
            if (dependencies.requireModelMIPUpdate(staticModel, bind.staticBind, Model.TYPE(), null)
                    || bind.staticBind.getRequired() != null && dependencies.requireModelMIPUpdate(staticModel,
                            bind.staticBind, Model.REQUIRED(), null)) {
                // Compute new type validity if the value of the node might have changed OR the value of requiredness
                // might have changed
                typeValidity = validateType(bind, currentNodeInfo, isRequired);
                bind.setTypeValidity(position, typeValidity);
            } else {
                // Keep current value
                typeValidity = bindNode.typeValid();
            }
        } else {
            // Keep current value (defaults to true when no type attribute)
            typeValidity = bindNode.typeValid();
        }

        // 2. Check required validity
        // We compute required validity every time
        final boolean requiredValidity;
        if (isRequired) {
            // Required
            final String nodeValue = DataModel.getValue(currentNodeInfo);
            requiredValidity = !isEmptyValue(nodeValue); // not valid if value is empty
        } else {
            // Not required, so any value passes including empty as far as required is
            // concerned
            requiredValidity = true;
        }
        bind.setRequiredValidity(position, requiredValidity);

        // Remember invalid instances
        if (!typeValidity || !requiredValidity) {
            final XFormsInstance instanceForNodeInfo = containingDocument.getInstanceForNode(currentNodeInfo);
            if (instanceForNodeInfo != null)
                invalidInstances.add(instanceForNodeInfo.getEffectiveId());
        }
    }

    private boolean validateType(RuntimeBind bind, NodeInfo currentNodeInfo, boolean required) {

        final boolean typeValid;
        {
            // NOTE: xf:bind/@type is a literal type value, and it is the same that applies to all nodes pointed to by xf:bind/@ref
            final QName typeQName = bind.typeQName;

            final String typeNamespaceURI = typeQName.getNamespaceURI();
            final String typeLocalname = typeQName.getName();

            // Get value to validate if not already computed above

            final String nodeValue = DataModel.getValue(currentNodeInfo);

            // TODO: "[...] these datatypes can be used in the type model item property without the addition of the
            // XForms namespace qualifier if the namespace context has the XForms namespace as the default
            // namespace."

            final boolean isBuiltInSchemaType = XMLConstants.XSD_URI.equals(typeNamespaceURI);
            final boolean isBuiltInXFormsType = XFormsConstants.XFORMS_NAMESPACE_URI.equals(typeNamespaceURI);
            final boolean isBuiltInXXFormsType = XFormsConstants.XXFORMS_NAMESPACE_URI.equals(typeNamespaceURI);

            if (isBuiltInXFormsType && Model.jXFormsSchemaTypeNames().contains(typeLocalname)) {
                // xf:dayTimeDuration, xf:yearMonthDuration, xf:email, xf:card-number
                if (xformsValidator == null) {
                    xformsValidator = new XFormsModelSchemaValidator("oxf:/org/orbeon/oxf/xforms/xforms-types.xsd");
                    xformsValidator.loadSchemas(containingDocument);
                }

                final String validationError = xformsValidator.validateDatatype(nodeValue, typeNamespaceURI,
                        typeLocalname, typeQName.getQualifiedName(), bind.staticBind.locationData());

                typeValid = validationError == null;

            } else if (isBuiltInXFormsType && nodeValue.length() == 0) {
                // Don't consider the node invalid if the string is empty with xf:* types
                typeValid = true;
            } else if (isBuiltInSchemaType || isBuiltInXFormsType) {
                // Built-in schema or XForms type

                // Use XML Schema namespace URI as Saxon doesn't know anything about XForms types
                final String newTypeNamespaceURI = XMLConstants.XSD_URI;

                // Get type information
                final int requiredTypeFingerprint = StandardNames.getFingerprint(newTypeNamespaceURI,
                        typeLocalname);
                if (requiredTypeFingerprint == -1) {
                    throw new ValidationException("Invalid schema type '" + bind.staticBind.dataTypeOrNull() + "'",
                            bind.staticBind.locationData());

                    // TODO: xxx check what XForms event must be dispatched
                }

                // Need an evaluator to check and convert type below
                final XPathEvaluator xpathEvaluator;
                try {
                    xpathEvaluator = new XPathEvaluator();
                    // NOTE: Not sure declaring namespaces here is necessary just to perform the cast
                    final IndependentContext context = (IndependentContext) xpathEvaluator.getStaticContext();
                    for (final Map.Entry<String, String> entry : bind.staticBind.namespaceMapping().mapping
                            .entrySet()) {
                        context.declareNamespace(entry.getKey(), entry.getValue());
                    }
                } catch (Exception e) {
                    throw OrbeonLocationException.wrapException(e, bind.staticBind.locationData());

                    // TODO: xxx check what XForms event must be dispatched
                }

                // Try to perform casting
                // TODO: Should we actually perform casting? This for example removes leading and trailing space around tokens. Is that expected?
                final StringValue stringValue = new StringValue(nodeValue);
                final XPathContext xpContext = new XPathContextMajor(stringValue, xpathEvaluator.getExecutable());
                final ConversionResult result = stringValue.convertPrimitive(
                        (BuiltInAtomicType) BuiltInType.getSchemaType(requiredTypeFingerprint), true, xpContext);

                // Set error on node if necessary
                typeValid = !(result instanceof ValidationFailure);
            } else if (isBuiltInXXFormsType) {
                // Built-in extension types
                final boolean isOptionalAndEmpty = !required && "".equals(nodeValue);
                if (typeLocalname.equals("xml")) {
                    // xxf:xml type
                    typeValid = isOptionalAndEmpty || XMLUtils.isWellFormedXML(nodeValue);
                } else if (typeLocalname.equals("xpath2")) {
                    // xxf:xpath2 type

                    // Find element which scopes namespaces
                    final NodeInfo namespaceNodeInfo;
                    if (currentNodeInfo.getNodeKind() == Node.ELEMENT_NODE)
                        namespaceNodeInfo = currentNodeInfo;
                    else
                        namespaceNodeInfo = currentNodeInfo.getParent();

                    if (namespaceNodeInfo != null && namespaceNodeInfo.getNodeKind() == Node.ELEMENT_NODE) {
                        // ASSUMPTION: Binding to dom4j-backed node (which InstanceData assumes too)
                        final Element namespaceElement = XML.unwrapElement(namespaceNodeInfo);
                        final NamespaceMapping namespaceMapping = new NamespaceMapping(
                                Dom4jUtils.getNamespaceContextNoDefault(namespaceElement));
                        typeValid = isOptionalAndEmpty || XPath.isXPath2Expression(nodeValue, namespaceMapping,
                                bind.staticBind.locationData(), indentedLogger);
                    } else {
                        // This means that we are bound to a node which is not an element and which does not have a
                        // parent element. This could be a detached attribute, or an element node, etc. Unsure if we
                        // would have made it this far anyway! We can't validate the expression so we only consider
                        // the "optional-and-empty" case.
                        typeValid = isOptionalAndEmpty;
                    }
                } else {
                    throw new ValidationException("Invalid schema type '" + bind.staticBind.dataTypeOrNull() + "'",
                            bind.staticBind.locationData());

                    // TODO: xxx check what XForms event must be dispatched
                }

            } else if (model.hasSchema()) {
                // Other type and there is a schema

                // There are possibly types defined in the schema
                final String validationError = model.getSchemaValidator().validateDatatype(nodeValue,
                        typeNamespaceURI, typeLocalname, typeQName.getQualifiedName(),
                        bind.staticBind.locationData());

                typeValid = validationError == null;
            } else {
                throw new ValidationException("Invalid schema type '" + bind.staticBind.dataTypeOrNull() + "'",
                        bind.staticBind.locationData());

                // TODO: xxx check what XForms event must be dispatched
            }
        }
        return typeValid;
    }

    public static boolean isEmptyValue(String value) {
        // TODO: configurable notion of "empty" through property (trimming vs. strict)
        return "".equals(value);
    }

    public static interface BindRunner {
        public void applyBind(RuntimeBind bind, int position);
    }

}