package org.openrosa.client.xforms; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Vector; import org.openrosa.client.model.Calculation; import org.openrosa.client.model.FormDef; import org.openrosa.client.model.GroupDef; import org.openrosa.client.model.IFormElement; import org.openrosa.client.model.OptionDef; import org.openrosa.client.model.QuestionDef; import org.openrosa.client.model.RepeatQtnsDef; import org.openrosa.client.util.ItextParser; import org.openrosa.client.util.FormUtil; import org.openrosa.client.xforms.XformConstants; import org.openrosa.client.xforms.XformUtil; import org.openrosa.client.xforms.XmlUtil; import org.openrosa.client.xpath.XPathExpression; import com.google.gwt.core.client.GWT; import com.google.gwt.xml.client.Document; import com.google.gwt.xml.client.Element; import com.google.gwt.xml.client.Node; import com.google.gwt.xml.client.NodeList; /** * Parse xforms documents and builds the form definition object model. * * @author daniel * */ public class XformParser { /** The current question id. */ private static int currentQuestionId = 1; /** The current page number. */ private static int currentPageNo = 1; /** * All methods in this class are static and hence we expect no external * Instantiation of this class. */ private XformParser(){ } /** * Gets a new question id. * * @return the new question id */ private static int getNextQuestionId(){ return currentQuestionId++; } /** * Gets a new page number. * * @return the new page number. */ private static int getNextPageNo(){ return currentPageNo++; } /** * Creates a copy of a formDef together with its xform xml. * * @param formDef the form to copy. * @return the new copy of the form. */ public static FormDef copyFormDef(FormDef formDef){ if(formDef.getDoc() == null) return new FormDef(formDef); else //Value of false creates bugs where repeat widgets are not loaded properly on data preview formDef.updateDoc(true); //formDef.updateDoc(false); return fromXform2FormDef(XformUtil.normalizeNameSpace(formDef.getDoc(),XmlUtil.fromDoc2String(formDef.getDoc()))); /*if(formDef.getDoc() == null) return new FormDef(formDef); else formDef.updateDoc(false); return fromXform2FormDef(XformUtil.normalizeNameSpace(formDef.getDoc(),XmlUtil.fromDoc2String(formDef.getDoc())));*/ } // /** // * Converts an xml document to a form definition object. // * // * @param xml the document xml. // * @return the form definition object. // */ // public static FormDef fromXform2FormDef(String xml, HashMap<Integer,HashMap<String,String>> languageText){ // Document doc = XmlUtil.getDocument(xml); // // String layoutXml = null, javaScriptSrc = null; NodeList nodes = null; // Element root = doc.getDocumentElement(); // if(root.getNodeName().equals("PurcForm")){ // nodes = root.getElementsByTagName("Xform"); // assert(nodes.getLength() > 0); // xml = XmlUtil.getChildElement(nodes.item(0)).toString(); // doc = XmlUtil.getDocument(xml); // // nodes = root.getElementsByTagName("Layout"); // if(nodes.getLength() > 0) // layoutXml = FormUtil.formatXml(XmlUtil.getChildElement(nodes.item(0)).toString()); // // nodes = root.getElementsByTagName("JavaScript"); // if(nodes.getLength() > 0) // javaScriptSrc = XmlUtil.getChildCDATA(nodes.item(0)).getNodeValue(); // // nodes = root.getElementsByTagName("LanguageText"); // assert(nodes.getLength() > 0); // } // // FormDef formDef = getFormDef(doc); // // if(layoutXml != null) // formDef.setLayoutXml(FormUtil.formatXml(layoutXml)); // // if(javaScriptSrc != null) // formDef.setJavaScriptSource(javaScriptSrc); // // if(nodes != null){ // loadLanguageText(formDef.getId(),nodes,languageText); // formDef.setXformXml(FormUtil.formatXml(xml)); // } // // return formDef; // } public static void loadLanguageText(Integer formId, NodeList nodes, HashMap<Integer,HashMap<String,String>> languageText){ for(int index = 0; index < nodes.getLength(); index++){ Element node = (Element)nodes.item(index); HashMap<String,String> map = languageText.get(formId); if(map == null){ map = new HashMap<String,String>(); languageText.put(formId, map); } map.put(node.getAttribute("lang"), FormUtil.formatXml(node.toString())); } } /** * Converts an xml document to a form definition object. * * @param xml the document xml. * @return the form definition object. */ public static FormDef fromXform2FormDef(String xml){ Document doc = XmlUtil.getDocument(xml); FormDef formDef = getFormDef(doc); formDef.setId(1); return formDef; } /** * Converts an xforms document into a form definition object and also * replaces its model with the given one. * * @param xformXml the xforms document xml. * @param modelXml the new xforms model xml. * @return the form definition object. */ public static FormDef fromXform2FormDef(String xformXml, String modelXml){ Document doc = XmlUtil.getDocument(xformXml); //If model xml has been supplied, use it to replace the existing one. if(modelXml != null){ Element node = XmlUtil.getDocument(modelXml).getDocumentElement();//XformConverter.getNode(XformConverter.getDocument(modelXml).getDocumentElement().toString()); Element dataNode = XformUtil.getInstanceDataNode(doc); Node parent = dataNode.getParentNode(); parent.appendChild(node); parent.replaceChild(node,dataNode); } return getFormDef(doc); } /** * Converts an xml document object to a form definition object. * * @param doc the xml document object. * @return the form definition object. */ public static FormDef getFormDef(Document doc){ Element rootNode = doc.getDocumentElement(); FormDef formDef = new FormDef(); formDef.setDoc(doc); formDef.setId(1); HashMap id2VarNameMap = new HashMap(); HashMap relevants = new HashMap(); HashMap constraints = new HashMap(); Vector repeats = new Vector(); HashMap rptKidMap = new HashMap(); List<QuestionDef> orphanDynOptionQns = new ArrayList<QuestionDef>(); currentQuestionId = 1; currentPageNo = 1; parseElement(formDef,rootNode,id2VarNameMap,null,relevants,repeats,rptKidMap,(int)0,null,constraints,orphanDynOptionQns); if(formDef.getName() == null || formDef.getName().length() == 0) formDef.setName(formDef.getQuestionID()); DefaultValueUtil.setDefaultValues(XformUtil.getInstanceDataNode(doc),formDef,id2VarNameMap); //TODO Very slow needs optimisation for very big forms RelevantParser.addSkipRules(formDef,relevants); ConstraintParser.addValidationRules(formDef,constraints); ItemsetParser.parseOrphanDynOptionQns(formDef,orphanDynOptionQns); //Remove all that we had created as questions when parsing bindings but will not require //user input (eg JR's DeviceId, EndTime), since questions are only for cases where we want user input. //TODO Needs to be fixed when having multiple groups // removeElementsWithoutControlNode(formDef.getChildren()); return formDef; } private static void removeElementsWithoutControlNode(List<IFormElement> elements){ if(elements == null) return; for(int index = 0; index < elements.size(); index++){ IFormElement element = elements.get(index); if(element.getControlNode() == null && !(element instanceof GroupDef)){ //element.getParent().removeChild(element); //We do not want to lose the bindings if any. GWT.log("removing node:"+element.getQuestionID()); element.getParent().getChildren().remove(element); index--; } else if(element instanceof GroupDef){ // removeElementsWithoutText(element.getChildren()); } } } /** * Parses an xforms document and builds a form definition object. * * @param formDef the form definition object that we are building. * @param element the element that we are currently parsing. * @param id2VarNameMap a map of question bind ids to the variable names. * @param questionDef the question definition object that is currently being parsed. * @param relevants the map of constraint attribute values keyed by their * question definition objects. * @param repeatQtns a list of repeat question types. * @param rptKidMap a map of question definition objects which are children of a repeat * question type, keyed by their variable names. * @param currentPageNo the number of the current page we are parsing. * @param parentQtn the parent of the question we are currently processing. * @param constraints the map of constraint attribute values keyed by their * question definition objects. * @param orphanDynOptionQns a list of dynamic option definition questions who parent * questions have not yet been parsed. * @return the question we are currently parsing. */ private static IFormElement parseElement(FormDef formDef, Element element, HashMap id2VarNameMap,IFormElement questionDef,HashMap relevants,Vector repeatQtns, HashMap rptKidMap, int currentPageNo, IFormElement parentQtn, HashMap constraints, List<QuestionDef> orphanDynOptionQns){ String label = ""; String hint = ""; String value = ""; Element labelNode = null; Element hintNode = null; Element valueNode = null; //TODO wiered bug here for some forms, nodes.getLength() returns a value less //than numOfEntries during the loop. So something could be changing the node list NodeList nodes = element.getChildNodes(); int numOfEntries = nodes.getLength(); for (int i = 0; i < numOfEntries; i++) { if(nodes.item(i) == null || nodes.item(i).getNodeType() != Node.ELEMENT_NODE) continue; Element child = (Element)nodes.item(i); String tagname = child.getNodeName(); //getNodeName(child); //if(tagname.equals(NODE_NAME_SUBMIT) || tagname.equals(NODE_NAME_SUBMIT_MINUS_PREFIX)) if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_SUBMIT_MINUS_PREFIX)) continue; else if (XmlUtil.nodeNameEquals(tagname,"head")) parseElement(formDef,child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); else if (XmlUtil.nodeNameEquals(tagname,"body")){ formDef.setBodyNode(child); parseElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); } else if (XmlUtil.nodeNameEquals(tagname,"title")){ formDef.setName(getText(child)); formDef.setItextId(XmlUtil.getItextId(child)); } //else if (tagname.equals(NODE_NAME_MODEL) || tagname.equals(NODE_NAME_MODEL_MINUS_PREFIX)){ else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_MODEL_MINUS_PREFIX)){ formDef.setModelNode((Element)child); formDef.setXformsNode(child.getOwnerDocument().getDocumentElement() /*child.getParentNode()*/); parseElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); } //else if (tagname.equals(NODE_NAME_GROUP) || tagname.equals(NODE_NAME_GROUP_MINUS_PREFIX)){ else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_GROUP_MINUS_PREFIX)){ /*questionDef = */parseGroupElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); } //else if(tagname.equals(NODE_NAME_INSTANCE)||tagname.equals(NODE_NAME_INSTANCE_MINUS_PREFIX)) { else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_INSTANCE_MINUS_PREFIX)){ parseInstanceElement(formDef, child); } //else if (tagname.equals(NODE_NAME_BIND)||tagname.equals(NODE_NAME_BIND_MINUS_PREFIX) /*|| tagname.equals(ATTRIBUTE_NAME_REF)*/) { else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_BIND_MINUS_PREFIX)){ IFormElement qtn = parseBindElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); qtn.setHasUINode(false); } //else if (tagname.equals(NODE_NAME_INPUT) || tagname.equals(NODE_NAME_SELECT1) || tagname.equals(NODE_NAME_SELECT) || tagname.equals(NODE_NAME_REPEAT) // || tagname.equals(NODE_NAME_INPUT_MINUS_PREFIX) || tagname.equals(NODE_NAME_SELECT1_MINUS_PREFIX) || tagname.equals(NODE_NAME_SELECT_MINUS_PREFIX) || tagname.equals(NODE_NAME_REPEAT_MINUS_PREFIX)) { else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_INPUT_MINUS_PREFIX) || XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_SELECT1_MINUS_PREFIX) || XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_SELECT_MINUS_PREFIX) || XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_UPLOAD_MINUS_PREFIX) || XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_TRIGGER_MINUS_PREFIX)){ NodeContext nodeContext = new NodeContext(label, hint, value, labelNode, hintNode, valueNode); questionDef = parseUiElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns,nodeContext); questionDef.setHasUINode(true); label = nodeContext.getLabel(); hint = nodeContext.getHint(); value = nodeContext.getValue(); labelNode = nodeContext.getLabelNode(); hintNode = nodeContext.getHintNode(); valueNode = nodeContext.getValueNode(); } else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_REPEAT_MINUS_PREFIX)){ // do nothing. Repeats are dealt with in parseGroupElement() } //else if(tagname.equals(NODE_NAME_ITEMSET)||tagname.equals(NODE_NAME_ITEMSET_MINUS_PREFIX)){ else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_ITEMSET_MINUS_PREFIX)){ questionDef.setDataType(QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC); ((QuestionDef)questionDef).setFirstOptionNode(child); ItemsetParser.parseDynamicOptionsList((QuestionDef)questionDef,child.getAttribute(XformConstants.ATTRIBUTE_NAME_NODESET),formDef,orphanDynOptionQns); } //else if(tagname.equals(NODE_NAME_LABEL)||tagname.equals(NODE_NAME_LABEL_MINUS_PREFIX)){ else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_LABEL_MINUS_PREFIX)){ NodeContext nodeContext = new NodeContext(label, hint, value, labelNode, hintNode, valueNode);; parseLabelElement(formDef, child, questionDef, nodeContext); if(questionDef instanceof GroupDef) setLabelValueNode(formDef, element, questionDef, parentQtn, nodeContext); label = nodeContext.getLabel(); hint = nodeContext.getHint(); value = nodeContext.getValue(); labelNode = nodeContext.getLabelNode(); hintNode = nodeContext.getHintNode(); valueNode = nodeContext.getValueNode(); } //else if (tagname.equals(NODE_NAME_HINT)||tagname.equals(NODE_NAME_HINT_MINUS_PREFIX)){ else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_HINT_MINUS_PREFIX)){ NodeContext nodeContext = new NodeContext(label, hint, value, labelNode, hintNode, valueNode);; parseHintElement(formDef, child, questionDef, nodeContext); label = nodeContext.getLabel(); hint = nodeContext.getHint(); value = nodeContext.getValue(); labelNode = nodeContext.getLabelNode(); hintNode = nodeContext.getHintNode(); valueNode = nodeContext.getValueNode(); } //else if (tagname.equals(NODE_NAME_ITEM)||tagname.equals(NODE_NAME_ITEM_MINUS_PREFIX)) else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_ITEM_MINUS_PREFIX)){ OptionDef optionDef = new OptionDef((QuestionDef)questionDef); parseElement(formDef, child,id2VarNameMap,optionDef,relevants,repeatQtns,rptKidMap,currentPageNo,questionDef,constraints,orphanDynOptionQns); //else if (tagname.equals(NODE_NAME_VALUE)||tagname.equals(NODE_NAME_VALUE_MINUS_PREFIX)){ }else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_VALUE_MINUS_PREFIX)){ if(true /*child.getChildNodes().getLength() != 0*/){ value = getText(child); if(value == null){ value = ""; } valueNode = child; } } else parseElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); // TODO - how are other elements like html:p or br handled? } NodeContext nodeContext = new NodeContext(label, hint, value, labelNode, hintNode, valueNode);; setLabelValueNode(formDef, element, questionDef, parentQtn, nodeContext); label = nodeContext.getLabel(); hint = nodeContext.getHint(); value = nodeContext.getValue(); labelNode = nodeContext.getLabelNode(); hintNode = nodeContext.getHintNode(); valueNode = nodeContext.getValueNode(); return questionDef; } /** * Sets the label and value nodes of the current object being parsed. * * @param formDef the form definition object. * @param element the element we are currently parsing. * @param questionDef the question we are currently parsing. * @param parentQtn the parent of the question we are currently processing. * @param nodeContext the node context. */ private static void setLabelValueNode(FormDef formDef, Element element, IFormElement questionDef, IFormElement parentQtn, NodeContext nodeContext){ boolean hasLabel = !nodeContext.getLabel().isEmpty() || ( (nodeContext.getLabelNode() != null) && (nodeContext.getLabelNode().getAttribute("ref") != null) && (!nodeContext.getLabelNode().getAttribute("ref").isEmpty()) ); boolean hasValue = !nodeContext.getValue().isEmpty(); if (hasLabel && hasValue) { if (parentQtn instanceof QuestionDef && parentQtn != null && parentQtn.getChildren() != null){ if(((QuestionDef)parentQtn).getOptions().size() == 0){ ((QuestionDef)parentQtn).setFirstOptionNode(element); } OptionDef option = ((OptionDef)questionDef); option.setId(Integer.parseInt(String.valueOf(parentQtn.getChildren().size()))); option.setText(nodeContext.getLabel()); option.setQuestionID(nodeContext.getValue()); option.setParent((QuestionDef)parentQtn); option.setControlNode(element); element.removeAttribute("id"); option.setLabelNode(nodeContext.getLabelNode()); option.setValueNode(nodeContext.getValueNode()); option.setDefaultValue(nodeContext.getValue()); ((QuestionDef)parentQtn).addOption(option); } } else if (hasLabel && questionDef != null){ if(questionDef.getText() == null || questionDef.getText().trim().isEmpty()){ if(questionDef != parentQtn && parentQtn instanceof GroupDef && questionDef.getParent() != parentQtn){ questionDef.getParent().getChildren().remove(questionDef); parentQtn.addChild(questionDef); } questionDef.setText(nodeContext.getLabel()); questionDef.setControlNode(element); questionDef.setLabelNode(nodeContext.getLabelNode()); if(questionDef instanceof QuestionDef) setQuestionDataNode((QuestionDef)questionDef,formDef,parentQtn); } if(nodeContext.getLabelNode() != null){ String ref = nodeContext.getLabelNode().getAttribute("ref"); if(ref != null && !ref.isEmpty()){ if(ref.contains("itext('")){ String[] refTokens = ref.split("'"); questionDef.setItextId(refTokens[1]); //refTokens should be in the form of [ "jr:itext(", "SOME_ID", ")" ] since we split on the ' char. } } } } } /** * Sets the xforms instance data node child of a given question definition object. * * @param qtn the question definition object. * @param formDef the form to which the question belongs. * @param parentQtn the parent question to which qtn belongs as a child. * This is only non null for kids of repeat question types. */ private static void setQuestionDataNode(IFormElement qtn, FormDef formDef, IFormElement parentQtn){ // String xpath = qtn.getQuestionID(); String xpath = qtn.getDataNodesetPath(); //xpath = new String(xpath.toCharArray(), 1, xpath.length()-1); int pos = xpath.lastIndexOf('@'); String attributeName = null; if(pos > 0){ attributeName = xpath.substring(pos+1,xpath.length()); xpath = xpath.substring(0,pos-1); } Element node = formDef.getDataNode(); if(qtn.getControlNode().getParentNode().getNodeName().equals(XformConstants.NODE_NAME_REPEAT)){ if(parentQtn != null) node = parentQtn.getDataNode(); } if(node == null) return; //data node may not be present in the xforms document. String nodeName = "/" + node.getNodeName() + "/"; if(xpath.contains(nodeName)){ xpath = xpath.substring(xpath.indexOf(nodeName) + nodeName.length()); } XPathExpression xpls = new XPathExpression(node, xpath); Vector result = xpls.getResult(); for (Enumeration e = result.elements(); e.hasMoreElements();) { Object obj = e.nextElement(); if (obj instanceof Element){ if(pos > 0) //Check if we are to set attribute value. qtn.setDataNode(((Element) obj)); //((Element) obj).setAttribute(attributeName, value); else qtn.setDataNode(((Element) obj));//((Element) obj).addChild(Node.TEXT_NODE, value); break; } } } // /** // * Checks if this is a repeat child question and adds it. // * @param qtn the questions to check // * @param repeats the list of repeat questions // * @return true if so, else false. // */ // private static boolean addRepeatChildQtn(IFormElement qtn, Vector repeats,Element child,HashMap map,HashMap rptKidmap){ // for(int i=0; i<repeats.size(); i++){ // QuestionDef rptQtn = (QuestionDef)repeats.get(i); // if(qtn.getBinding().contains(rptQtn.getBinding())){ // RepeatQtnsDef rptQtnsDef = rptQtn.getRepeatQtnsDef(); // //rptQtnsDef.addQuestion(qtn); //TODO This is temporarily removed to solve the wiered problem list bug // String varname = qtn.getBinding().substring(rptQtn.getBinding().length()+1); // //varname = varname.substring(0, varname.indexOf('/')); // //map.put(child.getAttribute(ATTRIBUTE_NAME_ID), varname); // map.put(varname, varname); // rptKidmap.put(varname, qtn); // return true; // } // } // return false; // } /** * Adds a new question that uses a ref attribute instead of bind. * * @param formDef the form definition object to which the question belongs. * @param child the node being currently processed. * @param relevants the map of constraint attribute values keyed by their * question definition objects. * @param ref the ref attribute value. * @param bind the bind attribute value. * @param constraints the map of constraint attribute values keyed by their * question definition objects. * @return the variable name of the new question. */ private static String addNonBindControl(FormDef formDef,Element child,HashMap relevants, String ref, String bind,HashMap constraints, IFormElement parentQtn){ if(parentQtn == null){ parentQtn = formDef; } QuestionDef qtn = new QuestionDef(null); qtn.setId(getNextQuestionId()); if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_TYPE) == null){ if(XmlUtil.nodeNameEquals(child.getNodeName(),XformConstants.NODE_NAME_TRIGGER_MINUS_PREFIX)){ qtn.setDataType(QuestionDef.QTN_TYPE_LABEL); }else{ qtn.setDataType(QuestionDef.QTN_TYPE_TEXT); } } else{ XformParserUtil.setQuestionType(qtn,child.getAttribute(XformConstants.ATTRIBUTE_NAME_TYPE),child); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_REQUIRED) != null && child.getAttribute(XformConstants.ATTRIBUTE_NAME_REQUIRED).equals(XformConstants.XPATH_VALUE_TRUE)){ if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_ACTION) == null){ qtn.setRequired(true); } } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_READONLY) != null && child.getAttribute(XformConstants.ATTRIBUTE_NAME_READONLY).equals(XformConstants.XPATH_VALUE_TRUE)){ qtn.setEnabled(false); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_LOCKED) != null && child.getAttribute(XformConstants.ATTRIBUTE_NAME_LOCKED).equals(XformConstants.XPATH_VALUE_TRUE)){ qtn.setLocked(true); } String qtnID = ((ref != null) ? ref : bind); String[] tokens = qtnID.split("/"); qtnID = tokens[tokens.length-1]; qtn.setQuestionID(qtnID); if(parentQtn instanceof GroupDef){ parentQtn.addChild(qtn); }else if(parentQtn.getDataType() == QuestionDef.QTN_TYPE_REPEAT){ ((QuestionDef)parentQtn).getRepeatQtnsDef().addChild(qtn); }else{ formDef.addChild(qtn); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_RELEVANT) != null){ relevants.put(qtn,child.getAttribute(XformConstants.ATTRIBUTE_NAME_RELEVANT)); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_CONSTRAINT) != null){ constraints.put(qtn,child.getAttribute(XformConstants.ATTRIBUTE_NAME_CONSTRAINT)); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_CALCULATE) != null){ formDef.addCalculation(new Calculation(qtn.getId(),child.getAttribute(XformConstants.ATTRIBUTE_NAME_CALCULATE))); } return qtn.getQuestionID(); } /** * Parses the instance node of an xforms document. * * @param formDef the form definition object that we are building. * @param child the element that we are currently parsing. */ private static void parseInstanceElement(FormDef formDef, Element child){ if(formDef.getDataNode() != null) return; //we only take the first instance node for formdef ref Element dataNode = null; for(int k=0; k<child.getChildNodes().getLength(); k++){ if(child.getChildNodes().item(k).getNodeType() == Node.ELEMENT_NODE){ dataNode = (Element)child.getChildNodes().item(k); formDef.setDataNode(dataNode); } } formDef.setQuestionID(XmlUtil.getNodeName(dataNode)); if(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_DESCRIPTION_TEMPLATE) != null) formDef.setDescriptionTemplate(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_DESCRIPTION_TEMPLATE)); if(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_ID) != null){ try{ formDef.setId(Integer.parseInt(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_ID))); } catch(Exception ex){/*We may have non numeric ids like for odk. We just ignore them.*/} } if(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_NAME) != null) formDef.setName(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_NAME)); if(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_FORM_KEY) != null) formDef.setFormKey(dataNode.getAttribute(XformConstants.ATTRIBUTE_NAME_FORM_KEY)); } /** * Parses a group element of an xforms document. * * @param formDef the form definition object that we are building. * @param element the element that we are currently parsing. * @param id2VarNameMap a map of question bind ids to the variable names. * @param questionDef the question definition object that is currently being parsed. * @param relevants the map of constraint attribute values keyed by their * question definition objects. * @param repeatQtns a list of repeat question types. * @param rptKidMap a map of question definition objects which are children of a repeat * question type, keyed by their variable names. * @param currentPageNo the number of the current page we are parsing. * @param parentQtn the parent of the question we are currently processing. * @param constraints the map of constraint attribute values keyed by their * question definition objects. * @param orphanDynOptionQns a list of dynamic option definition questions who parent * questions have not yet been parsed. */ private static void parseGroupElement(FormDef formDef, Element child, HashMap id2VarNameMap,IFormElement questionDef,HashMap relevants,Vector repeatQtns, HashMap rptKidMap, int currentPageNo, IFormElement parentQtn, HashMap constraints, List<QuestionDef> orphanDynOptionQns){ //Check to see if we're dealing with a REPEAT instead of a regular group. NodeList childNodes = child.getChildNodes(); for(int i=0;i<childNodes.getLength();i++){ if(childNodes.item(i).getNodeType() != Node.ELEMENT_NODE){ continue; } Element grpChild = (Element)childNodes.item(i); if(grpChild.getNodeType() == Element.TEXT_NODE){ continue; } if(XmlUtil.nodeNameEquals(grpChild.getNodeName(), "repeat")){ if(parentQtn == null){ parentQtn = formDef; } parseRepeatElement(formDef, child, id2VarNameMap, questionDef, relevants, repeatQtns, rptKidMap, currentPageNo, parentQtn, constraints, orphanDynOptionQns,i); // parseRepeatElement(formDef,parentQtn,questionDef,child,i); return; //short circuit regular group parsing. } } String variableName = XformParserUtil.getQuestionIDFromRefOrNodeset(child, formDef); GroupDef groupDef = new GroupDef(); QuestionDef groupBind = (QuestionDef)formDef.getElement(variableName); if(groupBind != null){ groupDef.setQuestionID(groupBind.getQuestionID()); groupDef.setBindNode(groupBind.getBindNode()); groupDef.setParent(groupBind.getParent()); groupDef.setId(groupBind.getId()); groupDef.setRequired(groupBind.isRequired()); // groupDef.setVisible(groupBind.isVisible()); groupDef.setEnabled(groupBind.isEnabled()); formDef.removeChild(groupBind); } groupDef.setControlNode(child); groupDef.setQuestionID(variableName); if(parentQtn == null){ formDef.addChild(groupDef); }else{ parentQtn.addChild(groupDef); } questionDef = groupDef; parentQtn = questionDef; parseElement(formDef, child,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); } private static void parseRepeatElement(FormDef formDef, Element child, HashMap id2VarNameMap,IFormElement questionDef,HashMap relevants,Vector repeatQtns, HashMap rptKidMap, int currentPageNo, IFormElement parentQtn, HashMap constraints, List<QuestionDef> orphanDynOptionQns, int repeatElementIndex){ String nodeset, jrCount; QuestionDef repeat = null; Element repeatNode = ((Element)child.getChildNodes().item(repeatElementIndex)); nodeset = repeatNode.getAttribute("nodeset"); jrCount = getJRCountAttributeValue(repeatNode); boolean hasLabelNode = ((Element)child).getElementsByTagName("label").getLength() > 0; Element labelNode = (Element)child.getElementsByTagName("label").item(0); boolean hasHintNode = ((Element)child).getElementsByTagName("hint").getLength() > 0; Element hintNode; String[] idTokens = nodeset.split("/"); String id = idTokens[idTokens.length-1]; IFormElement elementDef = formDef.getElement(id); if(id != null && elementDef != null){ repeat = (QuestionDef)elementDef; if(parentQtn != null){ parentQtn.getChildren().remove(repeat); }else{ formDef.getChildren().remove(repeat); } // formDef.removeChild(elementDef); } if(repeat == null){ repeat = new QuestionDef(parentQtn); } repeat.setQuestionID(id); repeat.setParent(parentQtn); repeat.setDataType(QuestionDef.QTN_TYPE_REPEAT); repeat.setHasUINode(true); if(hasLabelNode){ repeat.setText(XmlUtil.getTextValue(labelNode)); repeat.setLabelNode(labelNode); repeat.setItextId(XmlUtil.getItextId(labelNode)); } if(hasHintNode){ hintNode = (Element)child.getElementsByTagName("hint").item(0); repeat.setHintNode(hintNode); repeat.setHelpText(XmlUtil.getTextValue(hintNode)); } if(jrCount != null && !jrCount.isEmpty()){ repeat.setRepeatCountNodePath(jrCount); } repeat.setControlNode(((Element)child.getChildNodes().item(repeatElementIndex))); setQuestionDataNode((QuestionDef)repeat,formDef,parentQtn); RepeatQtnsDef repeatQtnsDef = new RepeatQtnsDef(repeat); repeat.setRepeatQtnsDef(repeatQtnsDef); parentQtn.addChild(repeat); questionDef = repeat; parentQtn = repeatQtnsDef; parseElement(formDef, repeatNode,id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); } /** * Convenience function, gets the jr:repeatCount attribute value from the node and returns it * @param repeatNode * @return */ private static String getJRCountAttributeValue(Element repeatNode){ return (repeatNode.getAttribute("jr:count") != null ? repeatNode.getAttribute("jr:count") : repeatNode.getAttribute("count")); } /** * Parses a bind element of an xforms document. * * @param formDef the form definition object that we are building. * @param element the element that we are currently parsing. * @param id2VarNameMap a map of question bind ids to the variable names. * @param questionDef the question definition object that is currently being parsed. * @param relevants the map of constraint attribute values keyed by their * question definition objects. * @param repeatQtns a list of repeat question types. * @param rptKidMap a map of question definition objects which are children of a repeat * question type, keyed by their variable names. * @param currentPageNo the number of the current page we are parsing. * @param parentQtn the parent of the question we are currently processing. * @param constraints the map of constraint attribute values keyed by their * question definition objects. * @param orphanDynOptionQns a list of dynamic option definition questions who parent * questions have not yet been parsed. * @return the question we are currently parsing. */ private static IFormElement parseBindElement(FormDef formDef, Element child, HashMap id2VarNameMap,IFormElement questionDef,HashMap relevants,Vector repeatQtns, HashMap rptKidMap, int currentPageNo, IFormElement parentQtn, HashMap constraints, List<QuestionDef> orphanDynOptionQns){ IFormElement def = new QuestionDef(null); def.setBindNode(child); def.setId(getNextQuestionId()); def.setQuestionID(FormUtil.getQtnIDFromElement(child)); if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_TYPE)!= null){ XformParserUtil.setQuestionType((QuestionDef)def,child.getAttribute(XformConstants.ATTRIBUTE_NAME_TYPE),child); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_REQUIRED) != null && child.getAttribute(XformConstants.ATTRIBUTE_NAME_REQUIRED).equals(XformConstants.XPATH_VALUE_TRUE)){ def.setRequired(true); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_READONLY) != null && child.getAttribute(XformConstants.ATTRIBUTE_NAME_READONLY).equals(XformConstants.XPATH_VALUE_TRUE)){ def.setEnabled(false); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_LOCKED) != null && child.getAttribute(XformConstants.ATTRIBUTE_NAME_LOCKED).equals(XformConstants.XPATH_VALUE_TRUE)){ def.setLocked(true); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_RELEVANT) != null){ relevants.put(def,child.getAttribute(XformConstants.ATTRIBUTE_NAME_RELEVANT)); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_CONSTRAINT) != null){ constraints.put(def,child.getAttribute(XformConstants.ATTRIBUTE_NAME_CONSTRAINT)); } if(child.getAttribute(XformConstants.ATTRIBUTE_NAME_CALCULATE) != null){ formDef.addCalculation(new Calculation(def.getId(),child.getAttribute(XformConstants.ATTRIBUTE_NAME_CALCULATE))); } child.removeAttribute("id"); if(child.getAttribute("type") != null && child.getAttribute("type").isEmpty()){ child.removeAttribute("type"); } formDef.addChild(def); return def; } /** * Parses a UI element of an xforms document. * * @param formDef the form definition object that we are building. * @param element the element that we are currently parsing. * @param id2VarNameMap a map of question bind ids to the variable names. * @param questionDef the question definition object that is currently being parsed. * @param relevants the map of constraint attribute values keyed by their * question definition objects. * @param repeatQtns a list of repeat question types. * @param rptKidMap a map of question definition objects which are children of a repeat * question type, keyed by their variable names. * @param currentPageNo the number of the current page we are parsing. * @param parentQtn the parent of the question we are currently processing. * @param constraints the map of constraint attribute values keyed by their * question definition objects. * @param orphanDynOptionQns a list of dynamic option definition questions who parent * questions have not yet been parsed. * @param nodeContext the current node context. * @return the question we are currently parsing. */ private static IFormElement parseUiElement(FormDef formDef, Element child, HashMap id2VarNameMap,IFormElement questionDef, HashMap relevants,Vector repeatQtns, HashMap rptKidMap, int currentPageNo, IFormElement parentQtn, HashMap constraints, List<QuestionDef> orphanDynOptionQns, NodeContext nodeContext){ String ref = child.getAttribute(XformConstants.ATTRIBUTE_NAME_REF); if(ref != null && ref.contains("jr:itext")){ ref = ref.replace("jr:itext('",""); // ref = ref.replace("')", ""); //remove incorrect jr:itext('ID') string ref = ref.replace(" ", "");//remove spaces (are not allowed) } String bind = child.getAttribute(XformConstants.ATTRIBUTE_NAME_BIND); if(ref == null && bind == null){ ref = child.getAttribute(XformConstants.ATTRIBUTE_NAME_NODESET); } //this appears to assume always relative nodeset paths (in Control nodes)... // String varName = (String)id2VarNameMap.get(((ref != null) ? ref : bind)); String varName = FormUtil.getQtnIDFromNodeSetPath(ref != null ? ref : bind); String tagname = child.getNodeName(); //new addition may cause bugs if(varName == null){ if(ref != null && ref.startsWith("/"+formDef.getQuestionID()+"/")){ String[] tokens = ref.split("/"); varName = tokens[tokens.length-1]; } if(formDef.getElement(varName) == null){ varName = addNonBindControl(formDef,child,relevants,ref,bind,constraints,parentQtn); } if(ref != null){ id2VarNameMap.put(ref, ref); } } if(varName != null){ IFormElement qtn = formDef.getElement(varName); if(qtn == null){ //what about questions without binds? qtn = (QuestionDef)rptKidMap.get(varName); } if(qtn == null){ varName = addNonBindControl(formDef, child, relevants, varName, bind, constraints, parentQtn); qtn = formDef.getElement(varName); } if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_SELECT1_MINUS_PREFIX) || XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_SELECT_MINUS_PREFIX)){ qtn.setDataType((XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_SELECT1_MINUS_PREFIX)) ? QuestionDef.QTN_TYPE_LIST_EXCLUSIVE : QuestionDef.QTN_TYPE_LIST_MULTIPLE); ((QuestionDef)qtn).setOptions(new Vector()); }else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_REPEAT_MINUS_PREFIX) && !nodeContext.getLabel().equals("")){ questionDef.setDataType(QuestionDef.QTN_TYPE_REPEAT); questionDef.setText(nodeContext.getLabel()); questionDef.setHelpText(nodeContext.getHint()); questionDef.setQuestionID(qtn.getQuestionID()); questionDef.setBindNode(qtn.getBindNode()); nodeContext.setLabel(""); nodeContext.setHint(""); questionDef.setLabelNode(nodeContext.getLabelNode()); questionDef.setHintNode(nodeContext.getHintNode()); questionDef.setControlNode(child); setQuestionDataNode(questionDef,formDef,parentQtn); parentQtn = questionDef; formDef.getChildren().remove(qtn); formDef.getChildren().remove(qtn); } else if(XmlUtil.nodeNameEquals(tagname,XformConstants.NODE_NAME_UPLOAD_MINUS_PREFIX)){ if("image/*".equalsIgnoreCase(child.getAttribute(XformConstants.ATTRIBUTE_NAME_MEDIATYPE))){ qtn.setDataType(QuestionDef.QTN_TYPE_IMAGE); }else if("audio/*".equalsIgnoreCase(child.getAttribute(XformConstants.ATTRIBUTE_NAME_MEDIATYPE))){ qtn.setDataType(QuestionDef.QTN_TYPE_AUDIO); }else if("video/*".equalsIgnoreCase(child.getAttribute(XformConstants.ATTRIBUTE_NAME_MEDIATYPE))){ qtn.setDataType(QuestionDef.QTN_TYPE_VIDEO); } } Element parent = (Element)child.getParentNode(); if(XmlUtil.nodeNameEquals(parent.getNodeName(),XformConstants.NODE_NAME_REPEAT_MINUS_PREFIX)){ varName = (String)id2VarNameMap.get(parent.getAttribute(XformConstants.ATTRIBUTE_NAME_BIND) != null ? parent.getAttribute(XformConstants.ATTRIBUTE_NAME_BIND) : parent.getAttribute(XformConstants.ATTRIBUTE_NAME_NODESET)); IFormElement rptQtnDef = formDef.getElement(varName); qtn.setId(getNextQuestionId()); // qtn.setBindNode(child); qtn.setControlNode(child); //Remove repeat question constraint if any XformParserUtil.replaceConstraintQtn(constraints,(QuestionDef)qtn); } if(XmlUtil.nodeNameEquals(child.getNodeName(),XformConstants.NODE_NAME_TRIGGER_MINUS_PREFIX)){ qtn.setDataType(QuestionDef.QTN_TYPE_LABEL); } questionDef = qtn; parseElement(formDef, child, id2VarNameMap,questionDef,relevants,repeatQtns,rptKidMap,currentPageNo,parentQtn,constraints,orphanDynOptionQns); } //For the children being oredered in the correct way (by appearance of the Control node in the xml doc). IFormElement parent = questionDef.getParent(); if(parent instanceof FormDef || parent instanceof GroupDef){ int numChildren = parent.getChildCount(); try{ parent.moveChildToIndex(questionDef, numChildren-1); //moves item to end of list. }catch(Exception e){ FormUtil.displayException(e); } } return questionDef; } /** * Parses a label element of an xforms document. * * @param formDef the form definition object that we are building. * @param child the element that we are currently parsing. * @param questionDef the question definition object that is currently being parsed. * @param nodeContext the current node context. */ private static void parseLabelElement(FormDef formDef, Element child, IFormElement questionDef, NodeContext nodeContext){ String parentName = ((Element)child.getParentNode()).getNodeName(); if(XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_INPUT_MINUS_PREFIX) || XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_SELECT_MINUS_PREFIX) || XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_SELECT1_MINUS_PREFIX) || XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_ITEM_MINUS_PREFIX) || XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_UPLOAD_MINUS_PREFIX) || XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_TRIGGER_MINUS_PREFIX)){ String label = XmlUtil.getTextValue(child); if(label == null) label = ""; nodeContext.setLabel(label); nodeContext.setLabelNode(child); } else if(XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_REPEAT_MINUS_PREFIX)){ if(questionDef != null){ questionDef.setText(XmlUtil.getTextValue(child)); if(getText(child)!=null){ questionDef.setItextId(getText(child)); } } } else if(XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_GROUP_MINUS_PREFIX)){ String label = XmlUtil.getTextValue(child); if(label == null) label = ""; nodeContext.setLabel(label); nodeContext.setLabelNode(child); } String ref = child.getAttribute("ref"); if(ref != null && !ref.isEmpty()){ if(ref.contains("itext('")){ String[] refTokens = ref.split("'"); questionDef.setItextId(refTokens[1]); //refTokens should be in the form of [ "jr:itext(", "SOME_ID", ")" ] since we split on the ' char. } } } /** * Parses a hint element of an xforms document. * * @param formDef the form definition object that we are building. * @param child the element that we are currently parsing. * @param questionDef the question definition object that is currently being parsed. * @param nodeContext the current node context. */ private static void parseHintElement(FormDef formDef, Element child, IFormElement questionDef, NodeContext nodeContext){ String parentName = ((Element)child.getParentNode()).getNodeName(); //if(parentName.equalsIgnoreCase(NODE_NAME_GROUP)||parentName.equalsIgnoreCase(NODE_NAME_GROUP_MINUS_PREFIX)){ if(XmlUtil.nodeNameEquals(parentName,XformConstants.NODE_NAME_GROUP_MINUS_PREFIX)){ if(true /*child.getChildNodes().getLength() != 0*/){ nodeContext.setHint(getText(child)); nodeContext.setHintNode(child); } } else if(questionDef != null){ questionDef.setHelpText(getText(child)); questionDef.setHintNode(child /*element*/); } } private static String getText(Element node){ if(node.getChildNodes().getLength() != 0){ String text = node.getChildNodes().item(0).getNodeValue().trim(); if(text.length() > 0) return text; } return null; } }