All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.javarosa.xform.parse.FormInstanceParser Maven / Gradle / Ivy

The newest version!
package org.javarosa.xform.parse;

import org.javarosa.core.model.Constants;
import org.javarosa.core.model.DataBinding;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.GroupDef;
import org.javarosa.core.model.IDataReference;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.ItemsetBinding;
import org.javarosa.core.model.QuestionDef;
import org.javarosa.core.model.condition.Constraint;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.core.model.instance.InvalidReferenceException;
import org.javarosa.core.model.instance.TreeElement;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.xform.util.XFormUtils;
import org.kxml2.kdom.Element;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.javarosa.xform.parse.XFormParser.buildInstanceStructure;
import static org.javarosa.xform.parse.XFormParser.getVagueLocation;

class FormInstanceParser {
    private static final Logger logger = LoggerFactory.getLogger(FormInstanceParser.class);

    private final FormDef formDef;
    private final String defaultNamespace;
    private final List bindings;
    private final List repeats;
    private final List itemsets;
    private final List selectOnes;
    private final List selectMultis;
    private final List actionTargets;
    /** pseudo-data model tree that describes the repeat structure of the instance; useful during instance processing and validation */
    private FormInstance repeatTree;

    FormInstanceParser(FormDef formDef, String defaultNamespace,
                       List bindings, List repeats, List itemsets,
                       List selectOnes, List selectMultis, List actionTargets) {
        // Todo: additional refactoring should shorten this too-long argument list
        this.formDef = formDef;
        this.defaultNamespace = defaultNamespace;
        this.bindings = bindings;
        this.repeats = repeats;
        this.itemsets = itemsets;
        this.selectOnes = selectOnes;
        this.selectMultis = selectMultis;
        this.actionTargets = actionTargets;
    }

    FormInstance parseInstance(Element e, boolean isMainInstance, String name, Map namespacePrefixesByUri) {
        TreeElement root = buildInstanceStructure(e, null, !isMainInstance ? name : null, e.getNamespace(),
                namespacePrefixesByUri, null);
        FormInstance instanceModel = new FormInstance(root);
        instanceModel.setName(isMainInstance ? formDef.getTitle() : name);

        final List usedAtts = Collections.unmodifiableList(Arrays.asList("id", "version", "uiVersion", "name",
                "prefix", "delimiter"));

        String schema = e.getNamespace();
        if (schema != null && schema.length() > 0 && !schema.equals(defaultNamespace)) {
            instanceModel.schema = schema;
        }
        instanceModel.formVersion = e.getAttributeValue(null, "version");
        instanceModel.uiVersion = e.getAttributeValue(null, "uiVersion");

        XFormParser.loadNamespaces(e, instanceModel);
        if (isMainInstance) {
            // the initialization of the references is done twice.
            // The first time is here because they are needed before these
            // validation steps can be performed.
            // It is then done again during the call to _f.setInstance().
            FormDef.updateItemsetReferences(formDef.getChildren());
            processRepeats(instanceModel);
            verifyBindings(instanceModel, e.getName());
            verifyActions(instanceModel);
        }
        applyInstanceProperties(instanceModel);

        //print unused attribute warning message for parent element
        if (XFormUtils.showUnusedAttributeWarning(e, usedAtts)){
            String xmlLocation = getVagueLocation(e);
            logger.warn("XForm Parse Warning: {}{}", XFormUtils.unusedAttWarning(e, usedAtts), (xmlLocation == null ? "" : xmlLocation));
        }

        return instanceModel;
    }

    /**
     * Pre-processes and cleans up instance regarding repeats; in particular:
     * 1) flags all repeat-related nodes as repeatable
     * 2) catalogs which repeat template nodes are explicitly defined, and notes which repeats bindings lack templates
     * 3) removes template nodes that are not valid for a repeat binding
     * 4) generates template nodes for repeat bindings that do not have one defined explicitly
     * 5) gives a stern warning for any repeated instance nodes that do not correspond to a repeat binding
     * 6) verifies that all sets of repeated nodes are homogeneous
     */
    private void processRepeats(FormInstance instance) {
        flagRepeatables(instance);
        processTemplates(instance);
        checkDuplicateNodesAreRepeatable(instance.getRoot());
        checkHomogeneity(instance);
    }

    /** Flags all nodes identified by repeat bindings as repeatable */
    private void flagRepeatables(FormInstance instance) {
        for (TreeReference ref : getRepeatableRefs()) {
            for (TreeReference nref : new EvaluationContext(instance).expandReference(ref, true)) {
                TreeElement node = instance.resolveReference(nref);
                if (node != null) { // catch '/'
                    node.setRepeatable(true);
                }
            }
        }
    }

    private void processTemplates (FormInstance instance) {
        repeatTree = buildRepeatTree(getRepeatableRefs(), instance.getRoot().getName());

        List missingTemplates = new ArrayList<>();
        checkRepeatsForTemplate(instance, repeatTree, missingTemplates);

        removeInvalidTemplates(instance, repeatTree);
        createMissingTemplates(instance, missingTemplates);
    }

    private void verifyBindings(FormInstance instance, String mainInstanceNodeName) {
        //check s (can't bind to '/', bound nodes actually exist)
        for (int i = 0; i < bindings.size(); i++) {
            DataBinding bind = bindings.get(i);
            TreeReference ref = FormInstance.unpackReference(bind.getReference());

            if (ref.size() == 0) {
                logger.info("Cannot bind to '/'; ignoring bind...");
                bindings.remove(i);
                i--;
            } else {
                List nodes = new EvaluationContext(instance).expandReference(ref, true);
                if (nodes.size() == 0) {
                    logger.warn("XForm Parse Warning:  defined for a node that doesn't exist " +
                        "[{}]. The node's name was probably changed and the bind should be updated.", ref.toString());
                }
            }
        }

        //check s (can't bind to '/' or '/data')
        for (TreeReference ref : getRepeatableRefs()) {
            if (ref.size() <= 1) {
                throw new XFormParseException("Cannot bind repeat to '/' or '/" + mainInstanceNodeName + "'");
            }
        }

        //check control/group/repeat bindings (bound nodes exist, question can't bind to '/')
        List bindErrors = new ArrayList<>();
        verifyControlBindings(formDef, instance, bindErrors);
        if (bindErrors.size() > 0) {
            String errorMsg = "";
            for (String bindError : bindErrors) {
                errorMsg += bindError + "\n";
            }
            throw new XFormParseException(errorMsg);
        }

        //check that repeat members bind to the proper scope (not above the binding of the parent repeat, and not within any sub-repeat (or outside repeat))
        verifyRepeatMemberBindings(formDef, instance, null);

        //check that label/copy/value refs are children of nodeset ref, and exist
        verifyItemsetBindings(instance);

        verifyItemsetSrcDstCompatibility(instance);
    }

    private void verifyActions (FormInstance instance) {
        //check the target of actions which are manipulating real values
        for (TreeReference target : actionTargets) {
            List nodes = new EvaluationContext(instance).expandReference(target, true);
            if (nodes.size() == 0) {
                throw new XFormParseException("Invalid Action - Targets non-existent node: " + target.toString(true));
            }
        }
    }

    private static void checkDuplicateNodesAreRepeatable (TreeElement node) {
        int mult = node.getMult();
        if (mult > 0) { //repeated node
            if (!node.isRepeatable()) {
                logger.warn("repeated nodes [{}] detected that have no repeat binding " +
                    "in the form; DO NOT bind questions to these nodes or their children!",
                    node.getName());
                //we could do a more comprehensive safety check in the future
            }
        }

        for (int i = 0; i < node.getNumChildren(); i++) {
            checkDuplicateNodesAreRepeatable(node.getChildAt(i));
        }
    }

    /** Checks repeat sets for homogeneity */
    private void checkHomogeneity (FormInstance instance) {
        for (TreeReference ref : getRepeatableRefs()) {
            TreeElement template = null;
            for (TreeReference nref : new EvaluationContext(instance).expandReference(ref)) {
                TreeElement node = instance.resolveReference(nref);
                if (node == null) //don't crash on '/'... invalid repeat binding will be caught later
                    continue;

                if (template == null)
                    template = instance.getTemplate(nref);

                if (!FormInstance.isHomogeneous(template, node)) {
                    logger.warn("XForm Parse Warning: Not all repeated nodes for a given repeat binding [{}] are homogeneous! This will cause serious problems!", nref.toString());
                }
            }
        }
    }

    private void verifyControlBindings (IFormElement fe, FormInstance instance, List errors) { //throws XmlPullParserException {
        if (fe.getChildren() == null)
            return;

        for (int i = 0; i < fe.getChildren().size(); i++) {
            IFormElement child = fe.getChildren().get(i);
            IDataReference ref = null;
            String type = null;

            if (child instanceof GroupDef) {
                ref = child.getBind();
                type = (((GroupDef)child).getRepeat() ? "Repeat" : "Group");
            } else if (child instanceof QuestionDef) {
                ref = child.getBind();
                type = "Question";
            }
            TreeReference tref = FormInstance.unpackReference(ref);

            if (child instanceof QuestionDef && tref.size() == 0) {
                //group can bind to '/'; repeat can't, but that's checked above
                logger.warn("XForm Parse Warning: Cannot bind control to '/'");
            } else {
                List nodes = new EvaluationContext(instance).expandReference(tref, true);
                if (nodes.size() == 0) {
                    logger.error("XForm Parse Error: {} bound to non-existent node: [{}]", type, tref.toString());
                    errors.add(type + " bound to non-existent node: [" + tref.toString() + "]");
                }
                //we can't check whether questions map to the right kind of node ('data' node vs. 'sub-tree' node) as that depends
                //on the question's data type, which we don't know yet
            }

            verifyControlBindings(child, instance, errors);
        }
    }

    private void verifyRepeatMemberBindings (IFormElement fe, FormInstance instance, GroupDef parentRepeat) {
        if (fe.getChildren() == null)
            return;

        for (int i = 0; i < fe.getChildren().size(); i++) {
            IFormElement child = fe.getChildren().get(i);
            boolean isRepeat = (child instanceof GroupDef && ((GroupDef)child).getRepeat());

            //get bindings of current node and nearest enclosing repeat
            TreeReference repeatBind = (parentRepeat == null ? TreeReference.rootRef() : FormInstance.unpackReference(parentRepeat.getBind()));
            TreeReference childBind = FormInstance.unpackReference(child.getBind());

            //check if current binding is within scope of repeat binding
            if (!repeatBind.isParentOf(childBind, false)) {
                //catch : repeat question is not a child of the repeated node
                throw new XFormParseException(" member's binding [" + childBind.toString() + "] is not a descendant of  binding [" + repeatBind.toString() + "]!");
            } else if (repeatBind.equals(childBind) && isRepeat) {
                //catch ... ( is ok)
                throw new XFormParseException("child s [" + childBind.toString() + "] cannot bind to the same node as their parent ; only questions/groups can");
            }

            //check that, in the instance, current node is not within the scope of any closer repeat binding
            //build a list of all the node's instance ancestors
            List repeatAncestry = new ArrayList<>();
            TreeElement repeatNode = (repeatTree == null ? null : repeatTree.getRoot());
            if (repeatNode != null) {
                repeatAncestry.add(repeatNode);
                for (int j = 1; j < childBind.size(); j++) {
                    repeatNode = repeatNode.getChild(childBind.getName(j), 0);
                    if (repeatNode != null) {
                        repeatAncestry.add(repeatNode);
                    } else {
                        break;
                    }
                }
            }
            //check that no nodes between the parent repeat and the target are repeatable
            for (int k = repeatBind.size(); k < childBind.size(); k++) {
                TreeElement rChild = (k < repeatAncestry.size() ? repeatAncestry.get(k) : null);
                boolean repeatable = rChild != null && rChild.isRepeatable();
                if (repeatable && !(k == childBind.size() - 1 && isRepeat)) {
                    //catch ......:
                    //  question's/group's/repeat's most immediate repeat parent in the instance is not its most immediate repeat parent in the form def
                    throw new XFormParseException(" member's binding [" + childBind.toString() + "] is within the scope of a  that is not its closest containing !");
                }
            }

            verifyRepeatMemberBindings(child, instance, (isRepeat ? (GroupDef)child : parentRepeat));
        }
    }

    private void verifyItemsetBindings (FormInstance instance) {
        for (ItemsetBinding itemset : itemsets) {
            //check proper parent/child relationship
            if (!itemset.nodesetRef.isParentOf(itemset.labelRef, false)) {
                throw new XFormParseException("itemset nodeset ref is not a parent of label ref");
            } else if (itemset.copyRef != null && !itemset.nodesetRef.isParentOf(itemset.copyRef, false)) {
                throw new XFormParseException("itemset nodeset ref is not a parent of copy ref");
            } else if (itemset.valueRef != null && !itemset.nodesetRef.isParentOf(itemset.valueRef, false)) {
                throw new XFormParseException("itemset nodeset ref is not a parent of value ref");
            }

            if (itemset.copyRef != null && itemset.valueRef != null) {
                if (!itemset.copyRef.isParentOf(itemset.valueRef, false)) {
                    throw new XFormParseException("itemset  is not a parent of ");
                }
            }

            //make sure the labelref is tested against the right instance
            //check if it's not the main instance
            DataInstance fi = null;
            if (itemset.labelRef.getInstanceName() != null) {
                fi = formDef.getNonMainInstance(itemset.labelRef.getInstanceName());
                if (fi == null) {
                    throw new XFormParseException("Instance: " + itemset.labelRef.getInstanceName() + " Does not exists");
                }
            } else {
                fi = instance;
            }


            if (fi.getTemplatePath(itemset.labelRef) == null) {
                throw new XFormParseException("




© 2015 - 2025 Weber Informatics LLC | Privacy Policy