/** * This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. * * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS * graphic logo is a trademark of OpenMRS Inc. */ package org.openmrs.module.xforms; import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.TreeMap; import java.util.TreeSet; import java.util.Vector; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.kxml2.kdom.Document; import org.kxml2.kdom.Element; import org.openmrs.Concept; import org.openmrs.ConceptAnswer; import org.openmrs.ConceptDatatype; import org.openmrs.ConceptName; import org.openmrs.ConceptNumeric; import org.openmrs.Field; import org.openmrs.Form; import org.openmrs.FormField; import org.openmrs.api.context.Context; import org.openmrs.module.xforms.formentry.FormEntryWrapper; import org.openmrs.module.xforms.formentry.FormSchemaFragment; import org.openmrs.module.xforms.util.XformsUtil; import org.openmrs.util.FormConstants; import org.openmrs.util.FormUtil; public class XformBuilderEx { private static Element bodyNode; public static Hashtable<String, Element> bindings; private static Hashtable<FormField, Element> formFields; private static Hashtable<FormField, String> fieldTokens; private static boolean useConceptIdAsHint = false; /** * Builds an xform for an given an openmrs form. * * @param form - the form object. * @return - the xml content of the xform. */ public static String buildXform(Form form) throws Exception { bindings = new Hashtable<String, Element>(); formFields = new Hashtable<FormField, Element>(); fieldTokens = new Hashtable<FormField, String>(); useConceptIdAsHint = "true".equalsIgnoreCase(Context.getAdministrationService().getGlobalProperty("xforms.useConceptIdAsHint")); boolean includeRelationshipNodes = !"false".equals(Context.getAdministrationService() .getGlobalProperty(XformConstants.GLOBAL_PROP_KEY_INCLUDE_PATIENT_RELATIONSHIPS)); //String schemaXml = XformsUtil.getSchema(form); String templateXml = FormEntryWrapper.getFormTemplate(form); //Add relationship data node if (includeRelationshipNodes) { templateXml = templateXml.replace("</patient>", " <patient_relative>\n <patient_relative.person/>\n <patient_relative.relationship/>\n </patient_relative>\n </patient>"); } Element formNode = (Element) XformBuilder.getDocument(new StringReader(templateXml)).getRootElement(); formNode.setAttribute(null, XformBuilder.ATTRIBUTE_UUID, form.getUuid()); Document doc = new Document(); doc.setEncoding(XformConstants.DEFAULT_CHARACTER_ENCODING); Element xformsNode = doc.createElement(XformBuilder.NAMESPACE_XFORMS, null); xformsNode.setName(XformBuilder.NODE_XFORMS); xformsNode.setPrefix(XformBuilder.PREFIX_XFORMS, XformBuilder.NAMESPACE_XFORMS); xformsNode.setPrefix(XformBuilder.PREFIX_XML_SCHEMA, XformBuilder.NAMESPACE_XML_SCHEMA); xformsNode.setPrefix(XformBuilder.PREFIX_XML_SCHEMA2, XformBuilder.NAMESPACE_XML_SCHEMA); xformsNode.setPrefix(XformBuilder.PREFIX_XML_INSTANCES, XformBuilder.NAMESPACE_XML_INSTANCE); xformsNode.setPrefix("jr", "http://openrosa.org/javarosa"); doc.addChild(org.kxml2.kdom.Element.ELEMENT, xformsNode); Element modelNode = doc.createElement(XformBuilder.NAMESPACE_XFORMS, null); modelNode.setName(XformBuilder.NODE_MODEL); modelNode.setAttribute(null, XformBuilder.ATTRIBUTE_ID, XformBuilder.MODEL_ID); xformsNode.addChild(Element.ELEMENT, modelNode); Element groupNode = doc.createElement(XformBuilder.NAMESPACE_XFORMS, null); groupNode.setName(XformBuilder.NODE_GROUP); Element labelNode = doc.createElement(XformBuilder.NAMESPACE_XFORMS, null); labelNode.setName(XformBuilder.NODE_LABEL); labelNode.addChild(Element.TEXT, "Page1"); groupNode.addChild(Element.ELEMENT, labelNode); xformsNode.addChild(Element.ELEMENT, groupNode); bodyNode = groupNode; Element instanceNode = doc.createElement(XformBuilder.NAMESPACE_XFORMS, null); instanceNode.setName(XformBuilder.NODE_INSTANCE); instanceNode.setAttribute(null, XformBuilder.ATTRIBUTE_ID, XformBuilder.INSTANCE_ID); modelNode.addChild(Element.ELEMENT, instanceNode); instanceNode.addChild(Element.ELEMENT, formNode); Document xformSchemaDoc = new Document(); xformSchemaDoc.setEncoding(XformConstants.DEFAULT_CHARACTER_ENCODING); Element xformSchemaNode = doc.createElement(XformBuilder.NAMESPACE_XML_SCHEMA, null); xformSchemaNode.setName(XformBuilder.NODE_SCHEMA); xformSchemaDoc.addChild(org.kxml2.kdom.Element.ELEMENT, xformSchemaNode); //TODO This block should be replaced with using database field items instead of // parsing the template document. Hashtable<String, String> problemList = new Hashtable<String, String>(); Hashtable<String, String> problemListItems = new Hashtable<String, String>(); XformBuilder.parseTemplate(modelNode, formNode, formNode, bindings, groupNode, problemList, problemListItems, 0); buildUInodes(form); //find all conceptId attributes in the document and replace their value with a mapped concept String prefSourceName = Context.getAdministrationService().getGlobalProperty( XformConstants.GLOBAL_PROP_KEY_PREFERRED_CONCEPT_SOURCE); //we only use the mappings if the global property is set if (StringUtils.isNotBlank(prefSourceName)) { for (int i = 0; i < formNode.getChildCount(); i++) { Element childElement = formNode.getElement(i); if (childElement != null) { for (int j = 0; j < childElement.getChildCount(); j++) { if (childElement.getElement(j) != null) { Element grandChildElement = childElement.getElement(j); String value = grandChildElement.getAttributeValue(null, XformBuilder.ATTRIBUTE_OPENMRS_CONCEPT); if (StringUtils.isNotBlank(value)) XformBuilder.addConceptMapAttributes(grandChildElement, value); } } } } } if (includeRelationshipNodes) { RelativeBuilder.build(modelNode, groupNode, formNode); } bindings.clear(); formFields.clear(); fieldTokens.clear(); return XformBuilder.fromDoc2String(doc); } public static void simpleConcept(String token, Concept concept, String type, boolean required, Locale locale, FormField formField) { addUInode(token, concept, type, XformBuilder.CONTROL_INPUT, locale, getParentNode(formField, locale)); } public static void dateConcept(String token, Concept concept, boolean required, Locale locale, FormField formField) { addUInode(token, concept, XformBuilder.DATA_TYPE_DATE, XformBuilder.CONTROL_INPUT, locale, getParentNode(formField, locale)); } public static void dateTimeConcept(String token, Concept concept, boolean required, Locale locale, FormField formField) { addUInode(token, concept, XformBuilder.DATA_TYPE_DATETIME, XformBuilder.CONTROL_INPUT, locale, getParentNode(formField, locale)); } public static void timeConcept(String token, Concept concept, boolean required, Locale locale, FormField formField) { addUInode(token, concept, XformBuilder.DATA_TYPE_TIME, XformBuilder.CONTROL_INPUT, locale, getParentNode(formField, locale)); } public static void numericConcept(String token, ConceptNumeric concept, boolean required, Locale locale, FormField formField) { addUInode(token, concept, XformBuilder.DATA_TYPE_DECIMAL, XformBuilder.CONTROL_INPUT, locale, getParentNode(formField, locale)); } public static void selectSingle(String token, Concept concept, Collection<ConceptAnswer> answerList, boolean required, Locale locale, FormField formField) { Element controlNode = addUInode(token, concept, XformBuilder.DATA_TYPE_TEXT, XformBuilder.CONTROL_SELECT1, locale, getParentNode(formField, locale)); if (controlNode != null) { addCodedUInodes(false, controlNode, answerList, concept, XformBuilder.DATA_TYPE_TEXT, XformBuilder.CONTROL_SELECT1, locale); } } public static void selectMultiple(String token, Concept concept, Collection<ConceptAnswer> answerList, Locale locale, FormField formField) { Element controlNode = addUInode(token, concept, XformBuilder.DATA_TYPE_TEXT, XformBuilder.CONTROL_SELECT, locale, getParentNode(formField, locale)); if (controlNode != null) { addCodedUInodes(true, controlNode, answerList, concept, XformBuilder.DATA_TYPE_TEXT, XformBuilder.CONTROL_SELECT, locale); } } public static void booleanConcept(String token, Concept concept, boolean required, Locale locale, FormField formField) { Element controlNode = addUInode(token, concept, XformBuilder.DATA_TYPE_BOOLEAN, XformBuilder.CONTROL_INPUT, locale, getParentNode(formField, locale)); /*if (controlNode != null) { //addCodedUInodes(false, controlNode, answerList, concept, XformBuilder.DATA_TYPE_TEXT, XformBuilder.CONTROL_INPUT, locale); Element itemNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); itemNode.setName(XformBuilder.NODE_ITEM); controlNode.addChild(Element.ELEMENT, itemNode); Element itemLabelNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); itemLabelNode.setName(XformBuilder.NODE_LABEL); itemLabelNode.addChild(Element.TEXT, conceptName); itemNode.addChild(Element.ELEMENT, itemLabelNode); //TODO This will make sense after the form designer's OptionDef implements //the xforms hint. //addHintNode(itemLabelNode, answer.getAnswerConcept()); Element itemValNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); itemValNode.setName(XformBuilder.NODE_VALUE); itemValNode.addChild(Element.TEXT, conceptValue); itemNode.addChild(Element.ELEMENT, itemValNode); }*/ } private static Element addUInode(String token, Concept concept, String dataType, String controlName, Locale locale, Element bodyNode){ String bindName = token; Element controlNode = bodyNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); controlNode.setName(controlName); controlNode.setAttribute(null, XformBuilder.ATTRIBUTE_BIND, bindName); Element bindNode = (Element) bindings.get(bindName); if (bindNode == null) { System.out.println("NULL bindNode for: " + bindName); return null; } bindNode.setAttribute(null, XformBuilder.ATTRIBUTE_TYPE, dataType); //create the label Element labelNode = bodyNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); labelNode.setName(XformBuilder.NODE_LABEL); ConceptName name = concept.getBestName(locale); if (name == null) { name = concept.getName(); } labelNode.addChild(Element.TEXT, name.getName()); controlNode.addChild(Element.ELEMENT, labelNode); addHintNode(labelNode, concept); XformBuilder.addControl(bodyNode, controlNode); if(concept instanceof ConceptNumeric) { ConceptNumeric numericConcept = (ConceptNumeric)concept; if(numericConcept.isPrecise()){ Double minInclusive = numericConcept.getLowAbsolute(); Double maxInclusive = numericConcept.getHiAbsolute(); if(!(minInclusive == null && maxInclusive == null)){ String lower = (minInclusive == null ? "" : FormSchemaFragment.numericToString(minInclusive, numericConcept.isPrecise())); String upper = (maxInclusive == null ? "" : FormSchemaFragment.numericToString(maxInclusive, numericConcept.isPrecise())); bindNode.setAttribute(null, XformBuilder.ATTRIBUTE_CONSTRAINT, ". >= " + lower + " and . <= " + upper); bindNode.setAttribute(null, (XformsUtil.isJavaRosaSaveFormat() ? "jr:constraintMsg" : XformBuilder.ATTRIBUTE_MESSAGE), "value should be between " + lower + " and " + upper + " inclusive"); } } } return controlNode; } private static void addCodedUInodes(boolean multiplSel, Element controlNode, Collection<ConceptAnswer> answerList, Concept concept, String dataType, String controlName, Locale locale){ for (ConceptAnswer answer : answerList) { String conceptName = answer.getAnswerConcept().getName(locale).getName(); String conceptValue; if (answer.getAnswerConcept().getConceptClass().getConceptClassId() .equals(FormConstants.CLASS_DRUG) && answer.getAnswerDrug() != null) { conceptName = answer.getAnswerDrug().getName(); if(multiplSel) conceptValue = FormUtil.getXmlToken(conceptName); else { conceptValue = StringEscapeUtils.escapeXml(FormUtil.conceptToString(answer.getAnswerConcept(), locale)) + "^" + FormUtil.drugToString(answer.getAnswerDrug()); } } else { if(multiplSel) conceptValue = FormUtil.getXmlToken(conceptName); else conceptValue = StringEscapeUtils.escapeXml(FormUtil.conceptToString(answer.getAnswerConcept(), locale)); } Element itemNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); itemNode.setName(XformBuilder.NODE_ITEM); itemNode.setAttribute(null, XformBuilder.ATTRIBUTE_CONCEPT_ID, concept.getConceptId().toString()); controlNode.addChild(Element.ELEMENT, itemNode); Element itemLabelNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); itemLabelNode.setName(XformBuilder.NODE_LABEL); itemLabelNode.addChild(Element.TEXT, conceptName); itemNode.addChild(Element.ELEMENT, itemLabelNode); //TODO This will make sense after the form designer's OptionDef implements //the xforms hint. //addHintNode(itemLabelNode, answer.getAnswerConcept()); Element itemValNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); itemValNode.setName(XformBuilder.NODE_VALUE); itemValNode.addChild(Element.TEXT, conceptValue); itemNode.addChild(Element.ELEMENT, itemValNode); } } private static Element getParentNode(FormField formField, Locale locale){ formField = formField.getParent(); if(formField == null){ return bodyNode; //is this problem list? } if(formField.getParent() == null){ return bodyNode; } else{ Element node = formFields.get(formField); if(node != null) return node; String token = fieldTokens.get(formField); Element groupNode = bodyNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); groupNode.setName(XformBuilder.NODE_GROUP); bodyNode.addChild(Element.ELEMENT, groupNode); Element labelNode = groupNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); labelNode.setName(XformBuilder.NODE_LABEL); labelNode.addChild(Element.TEXT, formField.getField().getConcept().getBestName(locale).getName()); groupNode.addChild(Element.ELEMENT, labelNode); addHintNode(labelNode, formField.getField().getConcept()); if (formField.getMaxOccurs() != null && formField.getMaxOccurs() == -1) { Element repeatControl = bodyNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); repeatControl.setName(XformBuilder.CONTROL_REPEAT); repeatControl.setAttribute(null, XformBuilder.ATTRIBUTE_BIND, token); groupNode.addChild(Element.ELEMENT, repeatControl); formFields.put(formField, repeatControl); return repeatControl; } else { groupNode.setAttribute(null, XformBuilder.ATTRIBUTE_ID, token); formFields.put(formField, groupNode); return groupNode; } } } public static void addProblemList(String token, Concept concept, boolean required, Locale locale, FormField formField) { Element groupNode = bodyNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); groupNode.setName(XformBuilder.NODE_GROUP); bodyNode.addChild(Element.ELEMENT, groupNode); Element labelNode = groupNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); labelNode.setName(XformBuilder.NODE_LABEL); labelNode.addChild(Element.TEXT, formField.getField().getConcept().getBestName(locale).getName()); groupNode.addChild(Element.ELEMENT, labelNode); addHintNode(labelNode, concept); Element repeatControl = bodyNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); repeatControl.setName(XformBuilder.CONTROL_REPEAT); repeatControl.setAttribute(null, XformBuilder.ATTRIBUTE_BIND, token); groupNode.addChild(Element.ELEMENT, repeatControl); //add the input node. Element controlNode = repeatControl.createElement(XformBuilder.NAMESPACE_XFORMS, null); controlNode.setName(XformBuilder.CONTROL_INPUT); String nodeset = "problem_list/" + token + "/value"; String id = nodeset.replace('/', '_'); controlNode.setAttribute(null, XformBuilder.ATTRIBUTE_BIND, id); repeatControl.addChild(Element.ELEMENT, controlNode); //add the label. labelNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); labelNode.setName(XformBuilder.NODE_LABEL); labelNode.addChild(Element.TEXT, token + " value"); controlNode.addChild(Element.ELEMENT, labelNode); addHintNode(labelNode, concept); //create bind node Element bindNode = controlNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); bindNode.setName(XformBuilder.NODE_BIND); bindNode.setAttribute(null, XformBuilder.ATTRIBUTE_ID, id); bindNode.setAttribute(null, XformBuilder.ATTRIBUTE_NODESET, "/form/" + nodeset); bindNode.setAttribute(null, XformBuilder.ATTRIBUTE_TYPE, XformBuilder.DATA_TYPE_TEXT); ((Element)bindings.get(token).getParent()).addChild(Element.ELEMENT, bindNode); } private static void buildUInodes(Form form) { Locale locale = Context.getLocale(); TreeMap<Integer, TreeSet<FormField>> formStructure = FormUtil.getFormStructure(form); buildUInodes(form, formStructure, 0, locale); } private static void buildUInodes(Form form, TreeMap<Integer, TreeSet<FormField>> formStructure, Integer sectionId, Locale locale) { if (!formStructure.containsKey(sectionId)) return; TreeSet<FormField> section = formStructure.get(sectionId); if (section == null || section.size() < 1) return; Vector<String> tagList = new Vector<String>(); for(FormField formField : section){ Integer subSectionId = formField.getFormFieldId(); String sectionName = FormUtil.getXmlToken(formField.getField().getName()); String name = FormUtil.getNewTag(sectionName, tagList); if(formField.getParent() != null && fieldTokens.values().contains(name)){ String parentName = fieldTokens.get(formField.getParent()); String token = parentName + "_" + name; if(!bindings.containsKey(token)) { token = FormUtil.getNewTag(FormUtil.getXmlToken(formField.getParent().getField().getName()), new Vector<String>()); token = token + "_" + name; } name = token; } fieldTokens.put(formField, name); Field field = formField.getField(); boolean required = formField.isRequired(); if (field.getFieldType().getFieldTypeId().equals( FormConstants.FIELD_TYPE_CONCEPT)) { Concept concept = field.getConcept(); ConceptDatatype datatype = concept.getDatatype(); if ( (name.contains("problem_added") || name.contains("problem_resolved")) && formField.getParent() != null && (formField.getParent().getField().getName().contains("PROBLEM LIST")) ){ addProblemList(name, concept, required, locale, formField); } else if (datatype.getHl7Abbreviation().equals(FormConstants.HL7_BOOLEAN)){ booleanConcept(name, concept, required, locale, formField); } else if (datatype.getHl7Abbreviation().equals(FormConstants.HL7_DATE)){ dateConcept(name, concept, required, locale, formField); } else if (datatype.getHl7Abbreviation().equals(FormConstants.HL7_DATETIME)){ dateTimeConcept(name, concept, required, locale, formField); } else if (datatype.getHl7Abbreviation().equals(FormConstants.HL7_TIME)){ timeConcept(name, concept, required, locale, formField); } else if (FormConstants.simpleDatatypes.containsKey(datatype.getHl7Abbreviation())){ simpleConcept(name, concept, XformBuilder.DATA_TYPE_TEXT, required, locale, formField); } else if (datatype.getHl7Abbreviation().equals(FormConstants.HL7_NUMERIC)) { ConceptNumeric conceptNumeric = Context.getConceptService().getConceptNumeric(concept.getConceptId()); numericConcept(name, conceptNumeric, required, locale, formField); } else if (datatype.getHl7Abbreviation().equals(FormConstants.HL7_CODED) || datatype.getHl7Abbreviation().equals(FormConstants.HL7_CODED_WITH_EXCEPTIONS)) { if (formField.getMaxOccurs() != null && formField.getMaxOccurs().intValue() == -1) { addProblemList(name, concept, required, locale, formField); } else { //Collection<ConceptAnswer> answers = concept.getAnswers(false); List answers = new ArrayList<ConceptAnswer>(concept.getAnswers(false)); if(answers != null && answers.size() > 0 && answers.get(0) instanceof Comparable) Collections.sort(answers); if (field.getSelectMultiple()){ selectMultiple(name, concept, answers, locale, formField); } else { selectSingle(name, concept, answers, required, locale, formField); } } } else if ("ED".equals(datatype.getHl7Abbreviation())){ simpleConcept(name, concept, XformBuilder.DATA_TYPE_BASE64BINARY, required, locale, formField); } } if (formStructure.containsKey(subSectionId)) { buildUInodes(form, formStructure, subSectionId, locale); } } } private static void addHintNode(Element labelNode, Concept concept) { String hint = null; if(concept.getDescription() != null) hint = concept.getDescription().getDescription(); if(useConceptIdAsHint) { hint = (hint != null ? hint + " [" + concept.getConceptId() + "]" : concept.getConceptId().toString()); } if(hint != null) { Element hintNode = labelNode.createElement(XformBuilder.NAMESPACE_XFORMS, null); hintNode.setName(XformBuilder.NODE_HINT); hintNode.addChild(Element.TEXT, hint); labelNode.getParent().addChild(Element.ELEMENT, hintNode); } } }