package org.openrosa.client.xforms; import java.util.List; import java.util.Vector; import org.openrosa.client.model.FormDef; import org.openrosa.client.model.IFormElement; import org.openrosa.client.model.QuestionDef; import org.openrosa.client.model.ModelConstants; import org.openrosa.client.xforms.XformConstants; import com.google.gwt.xml.client.NodeList; import com.google.gwt.xml.client.Document; import com.google.gwt.xml.client.Element; import com.google.gwt.xml.client.Node; /** * Utility methods used during the building of xforms documents. * * @author daniel * */ public class XformBuilderUtil { /** * All methods in this class are static and hence we expect no external * Instantiation of this class. */ private XformBuilderUtil(){ } /** * Converts from a question definition object type to its xsd type. * * @param type the QuestionDef data type. * @param node the node having the type attribute. * @return the xsd type. */ public static String getXmlType(int type, Element node){ if(node != null){ if(type == QuestionDef.QTN_TYPE_VIDEO) node.setAttribute(XformConstants.ATTRIBUTE_NAME_FORMAT,XformConstants.ATTRIBUTE_VALUE_VIDEO); else if(type == QuestionDef.QTN_TYPE_AUDIO) node.setAttribute(XformConstants.ATTRIBUTE_NAME_FORMAT, XformConstants.ATTRIBUTE_VALUE_AUDIO); else if(type == QuestionDef.QTN_TYPE_IMAGE) node.setAttribute(XformConstants.ATTRIBUTE_NAME_FORMAT, XformConstants.ATTRIBUTE_VALUE_IMAGE); else if(type == QuestionDef.QTN_TYPE_GPS) node.setAttribute(XformConstants.ATTRIBUTE_NAME_FORMAT, XformConstants.ATTRIBUTE_VALUE_GPS); } switch(type){ case QuestionDef.QTN_TYPE_BOOLEAN: return XformConstants.DATA_TYPE_BOOLEAN; case QuestionDef.QTN_TYPE_DATE: return XformConstants.DATA_TYPE_DATE; case QuestionDef.QTN_TYPE_DATE_TIME: return XformConstants.DATA_TYPE_DATE_TIME; case QuestionDef.QTN_TYPE_TIME: return XformConstants.DATA_TYPE_TIME; case QuestionDef.QTN_TYPE_DECIMAL: return XformConstants.DATA_TYPE_DECIMAL; case QuestionDef.QTN_TYPE_NUMERIC: return XformConstants.DATA_TYPE_INT; case QuestionDef.QTN_TYPE_LONG: return XformConstants.DATA_TYPE_LONG; case QuestionDef.QTN_TYPE_TEXT: return XformConstants.DATA_TYPE_TEXT; case QuestionDef.QTN_TYPE_LIST_EXCLUSIVE: return "select1"; case QuestionDef.QTN_TYPE_LIST_MULTIPLE: return "select"; case QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC: return ""; case QuestionDef.QTN_TYPE_GPS: return "geopoint"; //XformConstants.DATA_TYPE_TEXT; case QuestionDef.QTN_TYPE_IMAGE: case QuestionDef.QTN_TYPE_VIDEO: case QuestionDef.QTN_TYPE_AUDIO: return XformConstants.DATA_TYPE_BINARY; case QuestionDef.QTN_TYPE_BARCODE: return XformConstants.DATA_TYPE_BARCODE; } return ""; } /** * Converts an operator, for combining more than one condition, to its xforms representation. * * @param operator the operator numeric value. * @return the operator xforms text. */ public static String getConditionsOperatorText(int operator){ String operatorText = null; if(operator == ModelConstants.CONDITIONS_OPERATOR_AND) operatorText = XformConstants.CONDITIONS_OPERATOR_TEXT_AND; else if(operator == ModelConstants.CONDITIONS_OPERATOR_OR) operatorText = XformConstants.CONDITIONS_OPERATOR_TEXT_OR; return /*" " +*/ operatorText /*+ " "*/; } /** * Creates an xforms instance data child node from a given question variable name. * * @param doc the xforms document. * @param variableName the question variable name. * @param formDef the form definition object. * @param parentDataNode the xforms instance data node. * @return the instance data child node for the question. */ public static Element fromVariableName2Node(Document doc, String variableName,FormDef formDef,Element parentDataNode, IFormElement currentQuestion, IFormElement parentQuestion){ String name = variableName; //TODO May need to be smarter than this. Avoid invalid node //names. eg those having slashes (form1/question1) if(name.startsWith(formDef.getQuestionID())) name = name.substring(formDef.getQuestionID().length()+1); //TODO Should do this for all invalid characters in node names. /*name = name.replace("/", ""); name = name.replace("\\", "");*/ name = name.replace(" ", ""); Vector<Element> nodes = new Vector<Element>(); int prevPos = 0; int pos = name.indexOf('/'); String s; while(pos > 0){ s = name.substring(prevPos, pos); nodes.add(doc.createElement(s)); prevPos = ++pos; pos = name.indexOf('/', pos); } if(nodes.size() > 0 && prevPos < name.length()){ s = name.substring(prevPos); nodes.add(doc.createElement(s)); } Element dataNode = null; if(nodes.size() == 0){ String[] tokens = name.split("/"); name = tokens[tokens.length-1]; dataNode = doc.createElement(name); if(!hasChildElementWithName(parentDataNode,dataNode.getNodeName())){ List<IFormElement> childrenDefs = parentQuestion.getChildren(); int childIndex = childrenDefs.indexOf(currentQuestion); insertNodeAtIndex(parentDataNode, parentDataNode.getChildNodes(), childIndex, dataNode); } } else{ //construct a tree of nodes as given by the path in variableName (e.g. foo/bar/bash/baz -> <foo><bar><bash><baz /></bash>....) Element parentNode = null; for(int i=0; i<nodes.size(); i++){ if(i==0){ dataNode = nodes.elementAt(i); if(!hasChildElementWithName(parentDataNode,dataNode.getNodeName())){ parentDataNode.appendChild(dataNode); } parentNode = dataNode; } else{ if(!hasChildElementWithName(parentNode,nodes.elementAt(i).getNodeName())){ parentNode.appendChild(nodes.elementAt(i)); } parentNode = nodes.elementAt(i); } } dataNode = nodes.elementAt(nodes.size()-1); } return dataNode; } /** * Inserts the given childNode /before/ the item at Index * applicable to Data and Control XML nodes only * if index > childrenDOMNodes.length || index < 0 it appends the child to the list and returns. */ public static void insertNodeAtIndex(Element parentDOMNode, NodeList childrenDOMNodes, int index, Element child){ if(index > childrenDOMNodes.getLength() || index < 0){ parentDOMNode.appendChild(child); return; } if(index == -1 || childrenDOMNodes.getLength() == 0){ parentDOMNode.appendChild(child); }else{ Node nearestSibling = getNearestSibling(index, parentDOMNode.getChildNodes()); if(nearestSibling != null){ parentDOMNode.insertBefore(child, nearestSibling); }else{ parentDOMNode.appendChild(child); } } } /** * Goes through childrenDOMNodes and gets the Node specified by index. (where index is * calculated by counting the number of NON-TEXT nodes). E.g. childrenDOMNodes.getLength == 15 * but there are only 5 non-text nodes in the list. Specifiying getNearestSibling(3,...) would * return the 3rd non-text node. If index is out of bounds of the ChildrenDOMNodes list it will * return the first or last, respectively, node regardless of its type * @param index * @param childrenDOMNodes * @return null, if such an index is not found or is > than the length of childrenDOMNodes */ private static Node getNearestSibling(int index, NodeList childrenDOMNodes){ int count = 0; int childrenLength = childrenDOMNodes.getLength(); if(index < 0){ return childrenDOMNodes.item(0); }else if(index >= childrenDOMNodes.getLength()){ return null; }else if(index == childrenDOMNodes.getLength() - 1){ return null; } for(int i=0;i<childrenDOMNodes.getLength();i++){ Node currentItem = childrenDOMNodes.item(i); if(count == index ){ return currentItem; } if(currentItem.getNodeType() != Element.ELEMENT_NODE){ continue; } count++; } return null; //since we got this far we should return null, such that the node gets appended to the end. } /** * If the node has a text node that is empty, or no text nodes, this will return true. * If text nodes are present but they are non empty, returns false. * @param element * @return */ public static boolean nodeHasNoOrEmptyTextNodeChildren(Element element){ for(int i=0;i<element.getChildNodes().getLength();i++){ Node curNode = element.getChildNodes().item(i); if(curNode.getNodeType() == Node.TEXT_NODE){ String s = curNode.getNodeValue(); if(s.trim().isEmpty()){ return true;} //there's an edge case here, where if you run in Firefox and element.getNodeValue() has more than 4096 whitespaces this might not be correct. Don't do that. else{ return false; } } } return true; } /** * Checks to see if the Specified parent Element has a child (1st generation children only!) * with the name specified (where name is the tag name of the element). * @param parent * @param name - CASE SENSITIVE! * @return true if name found, false if not. False if no children are present. */ public static boolean hasChildElementWithName(Element parent, String name){ if(!parent.hasChildNodes()){ return false; } for(int i=0;i<parent.getChildNodes().getLength();i++){ if((parent.getChildNodes().item(i)).getNodeType() == Element.TEXT_NODE){ continue; } Element child = (Element)parent.getChildNodes().item(i); if(child.getNodeName().equals(name)){ return true; } } return false; } /** * Gets the bind id attribute value from a question variable name. * * @param variableName the question variable name. * @param isRepeatKid set to true if the question is a child of some other repeat question type. * @return the binding id attribute value. */ public static String getBindIdFromVariableName(String variableName, boolean isRepeatKid){ String id = variableName; if(!isRepeatKid && variableName.contains("/")){ if(variableName.indexOf('/') == variableName.lastIndexOf('/')) id = variableName.substring(variableName.lastIndexOf('/')+1); //as one / eg encounter/encounter.encounter_datetime else id = variableName.substring(variableName.indexOf('/')+1,variableName.lastIndexOf('/')); //has two / eg obs/method_of_delivery/value } return id; } /** * Gets the xpath expression operator (e.g =,!=,>) from an operator constant (e.g 1,2,3). * * @param operator the operator constant. * @param action the skip or validation rule target action. * @return the xpath expression operator. */ public static String getXpathOperator(int operator, int action){ if(operator == ModelConstants.OPERATOR_EQUAL) return "="; else if(operator == ModelConstants.OPERATOR_NOT_EQUAL) return "!="; else if(operator == ModelConstants.OPERATOR_LESS) return "<"; else if(operator == ModelConstants.OPERATOR_GREATER) return ">"; else if(operator == ModelConstants.OPERATOR_LESS_EQUAL) return "<="; else if(operator == ModelConstants.OPERATOR_GREATER_EQUAL) return ">="; else if(operator == ModelConstants.OPERATOR_IS_NOT_NULL) return "!="; else if(operator == ModelConstants.OPERATOR_IS_NULL) return "="; return "="; } /** * Checks if a skip rule target action is in the positive or negative sense. * * @param action the target action. * @return true if positive, else false. */ public static boolean isPositiveAction(int action){ return ((action & ModelConstants.ACTION_ENABLE) != 0) || ((action & ModelConstants.ACTION_SHOW) != 0); } }