package com.openMap1.mapper.converters; import java.util.Hashtable; import java.util.Iterator; import java.util.Vector; import java.util.StringTokenizer; import org.w3c.dom.Element; import com.openMap1.mapper.AttributeDef; import com.openMap1.mapper.ElementDef; import com.openMap1.mapper.MappedStructure; import com.openMap1.mapper.MappingCondition; import com.openMap1.mapper.MaxMult; import com.openMap1.mapper.MinMult; import com.openMap1.mapper.NodeMappingSet; import com.openMap1.mapper.ObjMapping; import com.openMap1.mapper.ValueCondition; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.structures.MapperWrapper; import com.openMap1.mapper.util.GenUtil; import com.openMap1.mapper.util.XMLUtil; import com.openMap1.mapper.util.XSLOutputFile; import com.openMap1.mapper.writer.TemplateFilter; /** * Wrapper class for NHS CDA wire format * New simpler version started 17/8/12, which does not need to add and remove npfitlc:contentId elements * @author Robert * */ public class NHS_CDA_Wire2 extends NHS_CDA_Wrapper implements MapperWrapper{ //---------------------------------------------------------------------------------------- // Constructor //---------------------------------------------------------------------------------------- /** * * @param ms set of mappings which uses this wrapper transform * @param spare spare argument, just in case.... */ public NHS_CDA_Wire2(MappedStructure ms, Object spare) throws MapperException { super(ms,spare); } //---------------------------------------------------------------------------------------- // In-Wrapper Transform //---------------------------------------------------------------------------------------- /** * recursion down the CDA tree, applying the in-wrapper name change to each element * @param newTagName new tag name to be applied to this element * @param cdaElement current element in the cda tree * @param elDef current element definition in the full mapping set * @return the element to include in the in-wrapped XML */ protected Element constrainedElement(String newTagName, Element cdaElement,ElementDef elDef) throws MapperException { String cdaName = XMLUtil.getLocalName(cdaElement); if (!cdaName.equals(newTagName)) trace("constrained Element " + newTagName + " for " + cdaName); // move across the element, with the tag name it has been given, and its attributes Element constrainedEl = moveInElementOnly(newTagName,cdaElement); // add child Elements and recurse for (Iterator<Element> ie = XMLUtil.childElements(cdaElement).iterator();ie.hasNext();) { Element cdaChild = ie.next(); String childName = XMLUtil.getLocalName(cdaChild); trace("trying child " + childName); // find the child node in the mapping set which has this name as a node name, or has the cda name in its description ElementDef childElDef = getChildDef(elDef,cdaChild); // you may not find it, because the subtree has been pruned out of the 'full' mapping set if (childElDef != null) { trace("found"); Element constrainedChild = null; // for <text> elements, save the child subtree in a table if (childElDef.getName().equals("text")) constrainedChild = saveInputTextSubtree(cdaChild); // for all other elements, pass the correct child name down the recursion else constrainedChild = constrainedElement(childElDef.getName(),cdaChild,childElDef); constrainedEl.appendChild(constrainedChild); } } return constrainedEl; } /** * @param elDef * @param cdaName * @return a child ElementDef which either has the same node name as the CDA element, * or, if there are none with that name, has the CDA element name defined in its Description, * and which has a matching template id on a grandchild <templateId> node; * or null if none are found. * @throws MapperException if there is more than one is found */ private ElementDef getChildDef(ElementDef elDef,Element cdaEl) throws MapperException { String cdaName = cdaEl.getLocalName(); String foundNames = ""; String templateIds = ""; Vector<ElementDef> firstPass = new Vector<ElementDef>(); Vector<ElementDef> secondPass = new Vector<ElementDef>(); Vector<ElementDef> thirdPass = new Vector<ElementDef>(); // first pass - filter on CDA tag name alone. If there is a unique child which matches, take it boolean uniqueCandidate = false; for (Iterator<ElementDef> it = elDef.getChildElements().iterator(); it.hasNext();) { ElementDef candidate = it.next(); if (cdaTagName(candidate).equals(cdaName)) { firstPass.add(candidate); uniqueCandidate = ((candidate.getMaxMultiplicity() == MaxMult.ONE) && (candidate.getMinMultiplicity() == MinMult.ONE)); } } if ((firstPass.size() == 1) && uniqueCandidate) return firstPass.get(0); // second pass - filter on template id, if it exists for (Iterator<ElementDef> it = firstPass.iterator(); it.hasNext();) { ElementDef candidate = it.next(); //candidates must match the mapping set on their grandchild template id, if the mapping set defines one or the instance has one String mappingTemplateId = mappingGrandChildTemplateIdExtension(candidate); String instanceTemplateId = instanceGrandChildTemplateIdExtension(cdaEl,false); trace("Node " + candidate.getName() + " has grandchild template ids '" + mappingTemplateId + "' and '" + instanceTemplateId + "'"); // if there are no template ids, both the mapping method and the instance method return template id = "" if (mappingTemplateId.equals(instanceTemplateId)) { secondPass.add(candidate); templateIds = templateIds + "'" + mappingTemplateId + "' "; } } if (secondPass.size() == 1) return secondPass.get(0); // third pass - filter on typecode for (Iterator<ElementDef> it = secondPass.iterator(); it.hasNext();) { ElementDef candidate = it.next(); String typeCodeInInstance = cdaEl.getAttribute("typeCode"); String typeCodeInMappingSet = fixedValueInMappingSet("@typeCode",candidate); if (typeCodeInInstance.equals(typeCodeInMappingSet)) { thirdPass.add(candidate); foundNames = foundNames + candidate.getName() + ", "; } } if (thirdPass.size() == 1) return thirdPass.get(0); if (thirdPass.size() == 0) return null; // found = 0 is allowed (node removed from full mapping set), but found > 1 is not (ambiguity) throw new MapperException("Found " + thirdPass.size() + " child nodes of element '" + elDef.getName() + "' with names " + foundNames + " defined to have CDA tag name '" + cdaName + "' with template ids " + templateIds); } private String fixedValueInMappingSet(String path, ElementDef elDef) { String fixedValue = ""; NodeMappingSet nms = elDef.getNodeMappingSet(); if (nms != null) for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator();it.hasNext();) { ObjMapping objMapping = it.next(); for (Iterator<MappingCondition> iu = objMapping.getMappingConditions().iterator();iu.hasNext();) { MappingCondition mc = iu.next(); if (mc instanceof ValueCondition) { ValueCondition vc = (ValueCondition)mc; if (vc.getLeftPath().equals(path)) fixedValue = vc.getRightValue(); } } } return fixedValue; } /** * * @param elDef * @return the cda tag name for a result element; either the name of the * result element itself (with namespace prefix removed), or, if different, got from the description */ private String cdaTagName(ElementDef elDef) { // remove any namespace prefix from elementDef name, to compare local names String cdaName = getLocalName(elDef); // Description fields are 'CDA name:<name>' for nodes whose names need changing String prefix = "CDA name:"; String description = elDef.getDescription(); if ((description != null) && (description.startsWith(prefix))) cdaName = description.substring(prefix.length()); return cdaName; } /** * * @param elDef * @return name of the ElementDef, with any namespace prefix removed */ private String getLocalName(ElementDef elDef) { String cdaName = null; StringTokenizer st = new StringTokenizer(elDef.getName(),":"); while (st.hasMoreTokens()) cdaName = st.nextToken(); return cdaName; } //---------------------------------------------------------------------------------------- // Out-Wrapper Transform //---------------------------------------------------------------------------------------- /** * recursion down the translation result tree, applying the out-wrapper name change to each element * @param cdaTagName new tag name to be applied to this element * @param resultElement current element in the translation result tree * @param elDef current element definition in the full mapping set * @return the element to include in the out-wrapped XML (wire format CDA) */ protected Element outWrappedV3Element(String cdaTagName, Element resultElement, ElementDef elDef) throws MapperException { String tag = cdaTagName; String uri = resultElement.getNamespaceURI(); // create the element in the out-wrapped document Element cdaEl = outResultDoc.createElementNS(uri, tag); // copy across all attributes to the templated CDA element, including namespace attributes XMLUtil.copyAttributes(resultElement,cdaEl); // if there are no child elements, copy any text content if (XMLUtil.childElements(resultElement).size() == 0) XMLUtil.copyText(resultElement, cdaEl, outResultDoc); // add child Elements and recurse for (Iterator<Element> ie = XMLUtil.childElements(resultElement).iterator();ie.hasNext();) { Element resultChild = ie.next(); String resultName = XMLUtil.getLocalName(resultChild); // find the child node in the mapping set with the same local name ElementDef childElDef = getChildWithLocalName(elDef,resultName); if (childElDef == null) throw new MapperException("Cannot find child element definition for result node '" + resultName + "'"); Element cdaChild = null; // for text elements, recover the subtree from a hashtable stored by the input wrapper if (resultName.equals("text")) cdaChild = recoverInputTextSubtree(resultChild, "unknown CDA path"); // otherwise pass the correct cda name for the child down the recursion else cdaChild = outWrappedV3Element(cdaTagName(childElDef),resultChild,childElDef); cdaEl.appendChild(cdaChild); } return cdaEl; } ElementDef getChildWithLocalName(ElementDef elDef, String localName) { ElementDef child = null; for (Iterator<ElementDef> it = elDef.getChildElements().iterator();it.hasNext();) { ElementDef next = it.next(); if (getLocalName(next).equals(localName)) child = next; } return child; } //---------------------------------------------------------------------------------------- // XSLT versions of wrapper transforms, for inclusion in full XSLT transforms //---------------------------------------------------------------------------------------- /** * @param xout XSLT output being made * @param templateFilter a filter on the templates, implemented by XSLGeneratorImpl * append the templates and variables to be included in the XSL * to do the full transformation, to apply the wrapper transform in the 'in' direction. * Templates must have mode = "inWrapper" */ public void addWrapperInTemplates(XSLOutputFile xout, TemplateFilter templateFilter) throws MapperException { // 'false' means do not read hand-written templates from a file super.addWrapperInTemplates(xout, templateFilter,false); addGeneratedCDAWireTemplates(xout, true); // 'true' means in-wrapper } /** * @param xout XSLT output being made * @param templateFilter a filter on the templates to be included, implemented by XSLGeneratorImpl * append the templates and variables to be included in the XSL * to do the full transformation, to apply the wrapper transform in the 'out' direction. * Templates must have mode = "outWrapper" * @throws MapperException */ public void addWrapperOutTemplates(XSLOutputFile xout, TemplateFilter templateFilter) throws MapperException { // 'false' means do not read hand-written templates from a file super.addWrapperOutTemplates(xout, templateFilter,false); addGeneratedCDAWireTemplates(xout,false); // 'false' means out-wrapper } /** * add path-sensitive templates for the CDA wire form in-wrapper or out-wrapper transform * @param xout * @throws MapperException */ private void addGeneratedCDAWireTemplates(XSLOutputFile xout, boolean isInWrapper) throws MapperException { ElementDef root = ms().getRootElement(); // add a general template to copy nodes with no name change, and do not generate any specific ones to do so xout.topOut().appendChild(identityPathTemplate(xout, mode(isInWrapper))); // note which tag names may need to be changed, at some path where they occur cdaNamesToChange = new Hashtable<String,String>(); fullMappingSetNamesToChange = new Hashtable<String,String>(); addTagNamesToChange(root); // String = match condition of template; Element = template root element Hashtable<String,Element> templateMatches = new Hashtable<String,Element>(); boolean addNamePreservingTemplates = false; addWireTemplates(xout,root,null,isInWrapper, templateMatches,addNamePreservingTemplates); // null = parent tag name } /*for the in-wrapper transform, which cda tag names need to be changed at some paths * (maybe not all their paths) * String = CDA tag name; String = "1" */ private Hashtable<String,String> cdaNamesToChange; /*for the out-wrapper transform, which full mapping set tag names need to be changed at some paths * (maybe not all their paths) * String = full mapping set node name; String = "1" */ private Hashtable<String,String> fullMappingSetNamesToChange; /** * recursive descent of the full mapping set tree, noting which tag names (both cda tag names and * full mapping set tag names) need to be changed for some path at which they occur * @param root */ private void addTagNamesToChange(ElementDef elDef) { String cdaName = cdaTagName(elDef); if (!elDef.getName().equals(cdaName)) { cdaNamesToChange.put(cdaName, "1"); fullMappingSetNamesToChange.put(elDef.getName(), "1"); } for (Iterator<ElementDef> it = elDef.getChildElements().iterator();it.hasNext();) addTagNamesToChange(it.next()); } /** * * @param mappingSetName * @param cdaName * @param isInWrapper * @return true if this tag name may need a name change for some paths in the wrapper transform */ private boolean mayNeedNameChange(String mappingSetName, String cdaName, boolean isInWrapper) { boolean mayChange = false; if (isInWrapper) mayChange = (cdaNamesToChange.get(cdaName) != null); else mayChange = (fullMappingSetNamesToChange.get(mappingSetName) != null); return mayChange; } /** * @param isInWrapper * @return the appropriate mode for in and out wrapper templates */ protected String mode(boolean isInWrapper) { String mode = "outWrapper"; if (isInWrapper) mode="inWrapper"; return mode; } /** * recursive descent of the full mapping set, adding templates to change tag names where needed. * Tag names which need a name change for some path need to have a 'name change' template for * every path, even if it does not change the name for some paths. * Otherwise, the name change template would always have higher priority than the default no-change * template, but for some paths would do nothing. * @param xout * @param elDef * @param cdaParentName * @param isInWrapper * @param templateMatches * @param parentSwitchedOutMode * @throws MapperException */ private void addWireTemplates(XSLOutputFile xout, ElementDef elDef, String cdaParentName, boolean isInWrapper, Hashtable<String,Element> templateMatches, boolean addNamePreservingTemplates) throws MapperException { String cdaName = cdaTagName(elDef); String elName = elDef.getName(); // add templates which don't change tag names only if required to do so boolean addTemplates = ((addNamePreservingTemplates)||(mayNeedNameChange(elName,cdaName,isInWrapper))); /* add a name change template for those elements in the v3 namespace that need it for any of their paths, * and (if required) for those which never have a name change for any path * (where the cda name is always the same as the ElementDef name); * because of the namespace condition, there is no in or out wrapper template for messageType*/ if ((addTemplates) && (!GenUtil.inArray(cdaName, IN_NHS_NAMESPACE))) { if (isInWrapper) addWireInTemplate(xout,elDef,cdaName,cdaParentName,templateMatches); else addWireOutTemplate(xout,elDef,cdaName,templateMatches); } // recurse down the mapping set, to do the same for descendants for (Iterator<ElementDef> it = elDef.getChildElements().iterator();it.hasNext();) { ElementDef childDef = it.next(); addWireTemplates(xout,childDef,cdaName,isInWrapper, templateMatches,addNamePreservingTemplates); } } /** * * @param xout * @param elDef * @param cdaName * @param cdaParentName * @param templateMatches * @throws MapperException */ private void addWireInTemplate(XSLOutputFile xout,ElementDef elDef, String cdaName,String cdaParentName, Hashtable<String,Element> templateMatches) throws MapperException { String tagName = elDef.getName(); // tag name to add to the result boolean highPriority = false; // priority 2 if false, 3 if true String parentMatchCondition = ""; if (cdaParentName != null) parentMatchCondition ="[parent::" + V3PREFIX + ":" + cdaParentName + "]"; // if the node itself has a templateId, use that in the matching conditions String templateMatchCondition = ""; String nodeTemplateId = getTemplateId(elDef); int found = 0; if (nodeTemplateId != null) { templateMatchCondition = "[" + V3PREFIX + ":templateId/@extension='" + nodeTemplateId + "']"; highPriority = true; found++; } // look for templateId beneath any 1..1 child nodes to use in match conditions String childTemplateMatchCondition = ""; for (Iterator<ElementDef> it = elDef.getChildElements().iterator(); it.hasNext();) { ElementDef child = it.next(); if ((child.getMinMultiplicity() == MinMult.ONE) && (child.getMaxMultiplicity() == MaxMult.ONE)) { String childTemplateId = getTemplateId(child); if (childTemplateId != null) { childTemplateMatchCondition = "[" + V3PREFIX + ":" + cdaTagName(child) + "/" + V3PREFIX + ":templateId/@extension='" + childTemplateId + "']"; highPriority = true; found++; } } } if (found == 0) System.out.println("Node at path '" + elDef.getPath() + "' has no <templateId> node or 1..1 child elements with <templateId> nodes."); //find any typeCode condition for the match String typeCodeMatchCondition = ""; String typeCode = fixedValueInMappingSet("@typeCode", elDef); if (!typeCode.equals("")) { typeCodeMatchCondition = "[@typeCode='" + typeCode + "']"; } // collect together whatever match conditions have been found; logical 'and' of all [] String matchCondition = parentMatchCondition + templateMatchCondition + childTemplateMatchCondition + typeCodeMatchCondition; // find the path to the parent element String parentPath = ""; if (elDef.eContainer() instanceof ElementDef) parentPath = ((ElementDef)elDef.eContainer()).getPath(); // give higher priority to templates whose match condition includes a template id String priority = "2"; if (highPriority) priority = "3"; // add the path template, to match on the CDA name and add an element with the in-wrapped tag name addPathTemplate(xout, "inWrapper", cdaName, matchCondition, parentPath, tagName, tagName, priority, templateMatches); } /** * * @param elDef * @return the templateId directly beneath an ElementDef of the full mapping set, if it has one; null otherwise */ private String getTemplateId(ElementDef elDef) { String templateId = null; ElementDef template = elDef.getNamedChildElement("templateId"); if (template != null) { AttributeDef extension = template.getNamedAttribute("extension"); if (extension != null) { String desc = extension.getDescription(); if (desc.startsWith("fixed:")) templateId = desc.substring("fixed:".length()); } } return templateId; } /** * * @param xout * @param elDef * @param cdaName * @param templateMatches * * @throws MapperException */ private void addWireOutTemplate(XSLOutputFile xout,ElementDef elDef, String cdaName,Hashtable<String,Element> templateMatches) throws MapperException { String tagName = elDef.getName(); // tag name to match on, and add to the path // no conditions to match on String condition = ""; // find the path to the parent element String parentPath = ""; if (elDef.eContainer() instanceof ElementDef) parentPath = ((ElementDef)elDef.eContainer()).getPath(); // add the path template, to add an element with the CDA name addPathTemplate(xout, "outWrapper", tagName, condition, parentPath, tagName, cdaName, "2", templateMatches); } /** /** * names of template files for hand-coded parts of the wire wrapper in and out transforms; not used */ protected String wrapperXSLFileName(boolean isInWrapper) { return "not used"; } private void message(String s) {System.out.println(s);} }