
org.javarosa.xform.parse.FormInstanceParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opendatakit-javarosa Show documentation
Show all versions of opendatakit-javarosa Show documentation
A Java library for rendering forms that are compliant with ODK XForms spec
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