package com.openMap1.mapper.converters;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;
import org.eclipse.emf.ecore.EObject;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.openMap1.mapper.ElementDef;
import com.openMap1.mapper.MappedStructure;
import com.openMap1.mapper.MappingCondition;
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.FileUtil;
import com.openMap1.mapper.util.GenUtil;
import com.openMap1.mapper.util.XMLUtil;
import com.openMap1.mapper.util.XSLOutputFile;
import com.openMap1.mapper.writer.TemplateFilter;
/**
* abstract superclass of the two different NHS CDA wrapper transforms:
*
* (1) NHS_V3_Converter, for usual V3 with RMIMs
* (2) NHS_CDA_Wire2, for wire format CDA
*
* Collects together what they have in common
*
* @author Robert
*
*/
abstract public class NHS_CDA_Wrapper extends AbstractMapperWrapper implements MapperWrapper{
protected boolean tracing() {return false;}
protected static String topClassName = "ClinicalDocument";
public static String NHSPREFIX = "npfitlc";
public static String NHSURI = "NPFIT:HL7:Localisation";
public static String CONTENTID_ROOT = "2.16.840.1.113883.2.1.3.2.4.18.16";
public static String TEMPLATEID_ROOT = "2.16.840.1.113883.2.1.3.2.4.18.2";
public static String[] IN_NHS_NAMESPACE = {"messageType","contentId","recipientRoleCode"};
//----------------------------------------------------------------------------------------
// Constructor
//----------------------------------------------------------------------------------------
/**
*
* @param ms set of mappings which uses this wrapper transform
* @param spare spare argument, just in case....
*/
public NHS_CDA_Wrapper(MappedStructure ms, Object spare) throws MapperException
{
super(ms,spare);
}
/**
* @return the file extension of the outer document, with initial '*.'
*/
public String fileExtension() {return ("*.xml");}
/**
* @return the type of document transformed to and from;
* see static constants in class AbstractMapperWrapper.
*/
public int transformType() {return AbstractMapperWrapper.XML_TYPE;}
//----------------------------------------------------------------------------------------
// In-Wrapper Transform
//----------------------------------------------------------------------------------------
public Document transformIn(Object CDARootObj) throws MapperException
{
// initialise for saving subtrees under <text> nodes
keptSubtrees = new Hashtable<String,Element>();
keyIndex = 0;
if (!(CDARootObj instanceof Element)) throw new MapperException("CDA root is not an Element");
Element CDARoot = (Element)CDARootObj;
inResultDoc = XMLUtil.makeOutDoc();
String namespaceURI = CDARoot.getNamespaceURI();
if (namespaceURI == null) throw new MapperException("CDA root element has no namespace");
if (!namespaceURI.equals(CDAConverter.V3NAMESPACEURI))
throw new MapperException("CDA root element namespace '" + namespaceURI
+ "' is not the HL7 V3 namespace '" + CDAConverter.V3NAMESPACEURI + "'");
// do not change the top tag name, in case it is not 'ClinicalDocument' when simplifying CMETs
String newTagName = CDARoot.getLocalName();
ElementDef topElementDef = ms().getRootElement();
Element constrainedRoot = constrainedElement(newTagName,CDARoot,topElementDef);
inResultDoc.appendChild(constrainedRoot);
return inResultDoc;
}
abstract Element constrainedElement(String newTagName, Element CDARoot,ElementDef topElementDef) throws MapperException;
protected Element moveInElementOnly(String newTagName,Element cdaElement)
{
// keep elements in the same namespace (V3 or NPfIT)
Element constrainedEl = inResultDoc.createElementNS(cdaElement.getNamespaceURI(), newTagName);
// copy across all attributes to the constrained element, including namespace attributes
for (int a = 0; a < cdaElement.getAttributes().getLength();a++)
{
Attr at = (Attr)cdaElement.getAttributes().item(a);
constrainedEl.setAttribute(at.getName(), at.getValue());
}
// add RIM structural attributes with constant values to name parts
addRIMStructuralAttributes(constrainedEl);
// if there are no child elements, copy any text content
if (XMLUtil.childElements(cdaElement).size() == 0)
{
String text = XMLUtil.getText(cdaElement);
if ((text != null) && (text.length() > 0)) constrainedEl.appendChild(inResultDoc.createTextNode(text));
}
return constrainedEl;
}
/**
* add RIM structural attributes with constant values to name parts
* and to names with no parts
* @param el
*/
protected void addRIMStructuralAttributes(Element el)
{
String[] nameParts = {"name","prefix","given","family","suffix"};
String[] partTypes = {"","PFX","GIV","FAM","SFX"};
String tag = el.getLocalName();
if ((GenUtil.inArray(tag, nameParts)) && (XMLUtil.childElements(el).size() == 0))
{
el.setAttribute("mediaType","text/plain");
el.setAttribute("representation","TXT");
for (int i = 0; i < nameParts.length;i++)
if ((tag.equals(nameParts[i])) && (partTypes[i].length() > 0))
el.setAttribute("partType", partTypes[i]);
}
}
//----------------------------------------------------------------------------------------
// Out-Wrapper Transform
//----------------------------------------------------------------------------------------
public Document transformOut(Element constrainedRoot) throws MapperException {
outResultDoc = XMLUtil.makeOutDoc();
String namespaceURI = constrainedRoot.getNamespaceURI();
if (namespaceURI == null) throw new MapperException("CDA root element has no namespace");
if (!namespaceURI.equals(CDAConverter.V3NAMESPACEURI))
throw new MapperException("CDA root element namespace '" + namespaceURI
+ "' is not the HL7 V3 namespace '" + CDAConverter.V3NAMESPACEURI + "'");
// do not change the top tag name, in case it is not 'ClinicalDocument' when simplifying CMETs
String newTagName = constrainedRoot.getLocalName();
ElementDef topElementDef = ms().getRootElement();
Element cdaRoot = outWrappedV3Element(newTagName,constrainedRoot,topElementDef);
outResultDoc.appendChild(cdaRoot);
return outResultDoc;
}
abstract Element outWrappedV3Element(String newTagName,Element constrainedElement,ElementDef topElementDef) throws MapperException;
/**
*
* @param el an element in the message before out-wrapper transform
* @return if the element has a 'templateId' grandchild beneath
* a child node, the value of its 'extension' attribute.
* If hasInWrappedNodeNames is true, the child node must have a
* tag name like 'COCD_TP145022UK02.AssignedEntitySDS' with a template name before '.'
*
*/
protected String instanceGrandChildTemplateIdExtension(Element el, boolean hasInWrappedNodeNames)
{
String extension = "";
for (Iterator<Element> ie = XMLUtil.childElements(el).iterator();ie.hasNext();)
{
Element child = ie.next();
if ((!hasInWrappedNodeNames)|(isTemplateTag(child.getLocalName())))
{
Element templateId = XMLUtil.firstNamedChild(child, "templateId");
if (templateId != null) extension = templateId.getAttribute("extension");
}
}
return extension;
}
/**
*
* @param elDef an ElementDef node in a full mapping set
* @return if it has an object mapping with path
* like 'COCD_TP145022UK02.AssignedEntitySDS/templateId/@extension'
* the value of the condition which fixes the extension attribute,
* or if an object mapping requiring that value is on a parent or a child
* Otherwise return "".
*/
protected String mappingGrandChildTemplateIdExtension(ElementDef elDef)
{
/* the mappings which define the template id are usually on this node (Participation or ActRelationship),
* not on the child Act or Role */
String extension = getTemplateIdExtension(elDef,3,null);
// sometimes the relevant mappings can be on the parent ElementDef; but the path must go down through this node
if (extension.equals(""))
{
extension = getAncestorExtension(elDef,"",3);
}
// the relevant mappings can be on a child Act or Role, if the association has not been flattened
if (extension.equals("")) for (Iterator<ElementDef> it = elDef.getChildElements().iterator();it.hasNext();)
{
ElementDef child = it.next();
if (isTemplateTag(child.getName())) extension = getTemplateIdExtension(child,2,null);
}
return extension;
}
/**
* recursive ascent of ancestor ElementDefs, looking for an object mapping fixed value to define the grandchild template
* id of this node
* @param elDef
* @param path
* @param length
* @return
*/
private String getAncestorExtension(ElementDef elDef, String path, int length)
{
String extension = "";
String nextPath = elDef.getName() + "/" + path;
int nextLength = length + 1;
EObject outer = elDef.eContainer();
if (outer instanceof ElementDef)
{
ElementDef parent = (ElementDef)outer;
extension = getTemplateIdExtension(parent,nextLength,nextPath);
// if a node does not give a real extension, try its parent
if (extension.equals("")) extension = getAncestorExtension(parent,nextPath,nextLength);
}
return extension;
}
/**
* search all object mappings on a node for value conditions on templateId extensions,
* with a given length of the value condition path.
* If one is found, return the extension value
* @param elDef an ElementDef with maybe some object mappings on it
* @param pathLength an intyeger 3 or more
* @param requiredFirstNode - if non-null, the first node on the path must be this
* @return
*/
protected String getTemplateIdExtension(ElementDef elDef,int pathLength, String requiredFirstNode)
{
String extension = "";
NodeMappingSet nms = elDef.getNodeMappingSet();
if (nms != null)
{
// find the appropriate value condition on any object mapping
for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator();it.hasNext();)
{
ObjMapping om = it.next();
for (Iterator<MappingCondition> iu = om.getMappingConditions().iterator();iu.hasNext();)
{
MappingCondition mc = iu.next();
String ext = getTemplateIdExtension(mc,pathLength,requiredFirstNode);
if (!ext.equals("")) extension = ext;
}
}
}
return extension;
}
/**
* find the value of the attribute 'extension' of a templateId node,
* required for a value condition
* on an object mapping, with a given path length of the condition path
* @param mc
* @param pathLength an integer 3 or more
* @param requiredFirstNode - if non-null, the first node on the path must be this
* @return
*/
private String getTemplateIdExtension(MappingCondition mc, int pathLength, String requiredFirstNode)
{
String extension = "";
if (mc instanceof ValueCondition)
{
// find a value condition with path ending like 'COCD_TP145201GB01.PatientRole/templateId/@extension'
ValueCondition vc = (ValueCondition)mc;
StringTokenizer path = new StringTokenizer(vc.getLeftPath(),"/");
if (path.countTokens() == pathLength)
{
if (pathLength == 2)
{
if ((path.nextToken().equals("templateId"))
&& (path.nextToken().equals("@extension")))
extension = vc.getRightValue();
}
else if (pathLength > 2)
{
// strip off all except the last 3 steps
for (int i = 0; i < pathLength - 3; i++) path.nextToken();
// check the last 3 steps of the path
if ((isTemplateTag(path.nextToken()))
&& (path.nextToken().equals("templateId"))
&& (path.nextToken().equals("@extension")))
extension = vc.getRightValue();
}
// if there is a condition on the first node of the path, and it fails, return ""
if ((requiredFirstNode != null) && (!vc.getLeftPath().startsWith(requiredFirstNode))) extension = "";
}
}
return extension;
}
/**
* @param tagName
* @return true if the tag name starts with a template id, then a '.'
* This test has now been disabled to return true always; some cases do not obey it, and
* the other tests (looking for templateId/@extension) appear strict enough
*/
private boolean isTemplateTag(String tagName)
{
StringTokenizer childTag = new StringTokenizer(tagName,".");
@SuppressWarnings("unused")
boolean strictTemplateTag = ((childTag.countTokens() == 2) && (childTag.nextToken().length() == "COCD_TP145022UK02".length()));
return true;
}
/**
*
* @param theClass
* @return true if the class is a class that has its templateId
* on a grandchild node rather than a child node, and has a contentId child
*/
protected boolean renamableTag(Element el)
{
String type = el.getAttribute("typeCode");
return (type.length() > 0);
}
protected void giveContentIdChild(String ext,Element cdaEl, Element resultElement)
{
if ((ext != null) && (!ext.equals("")) && (renamableTag(resultElement))
&& (XMLUtil.firstNamedChild(resultElement, "contentId") == null))
cdaEl.appendChild(contentIdElement(outResultDoc,ext));
}
/**
*
* @param outDoc
* @param extension
* @return a contentId element in the NHS local namespace
*/
protected Element contentIdElement(Document outDoc, String extension)
{
Element contentIdEl = outDoc.createElementNS(NHSURI, NHSPREFIX + ":contentId");
contentIdEl.setAttribute("root", CONTENTID_ROOT);
contentIdEl.setAttribute("extension", extension);
return contentIdEl;
}
/**
*
* @param outDoc
* @param extension
* @return a templateId element in the V3 namespace
*/
protected Element templateIdElement(Document outDoc, String extension)
{
Element templateIdEl = outDoc.createElementNS(V3NAMESPACEURI, "templateId");
templateIdEl.setAttribute("root", TEMPLATEID_ROOT);
templateIdEl.setAttribute("extension", extension);
return templateIdEl;
}
//----------------------------------------------------------------------------------------
// 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"
*
* This method works by reading a file which is a standalone XSL version
* of the in-wrapper transform, removing the top template,
* then adding all other templates and variables to the full XSLT.
*
* XSLGenerator provides a variable:
<xsl:variable name="inWrapper_result">
<xsl:apply-templates mode="inWrapper" />
</xsl:variable>
which replaces the top template:
<xsl:template match="/">
<xsl:apply-templates mode="inWrapper"/>
</xsl:template>
* of the standalone XSLT file
* @throws MapperException
*/
public void addWrapperInTemplates(XSLOutputFile xout, TemplateFilter templateFilter, boolean addTemplatesFromFile)
throws MapperException
{
// declare namespaces at the top of the xslt
declareNamespaces(xout);
// alter generated templates to pass through subtrees under text nodes unchanged
passThroughTextSubtrees(xout,true);
if (addTemplatesFromFile)
{
/* the standalone NHS wrapper in-transform should be in a file NHSInWrapper.xsl,
* in a sub-folder of the same folder as the NHS mapper file. */
boolean isInWrapper = true;
String xslLocation = templateFilter.getXSLLocation(wrapperXSLFileName(isInWrapper),isInWrapper);
boolean removeStarterTemplate = true;
addTemplatesFromFile(xout,xslLocation,removeStarterTemplate);
}
}
/**
* @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, boolean addTemplatesFromFile)
throws MapperException
{
// declare namespaces at the top of the xslt
declareNamespaces(xout);
// alter generated templates to pass through subtrees under text nodes unchanged
passThroughTextSubtrees(xout,false);
if (addTemplatesFromFile)
{
/* the standalone NHS wrapper out-transform should be in a file NHSOutWrapper.xsl,
* in a subfolder of the same folder as the mapper file. */
boolean isInWrapper = false;
String xslLocation = templateFilter.getXSLLocation(wrapperXSLFileName(isInWrapper),isInWrapper);
boolean removeStarterTemplate = true;
addTemplatesFromFile(xout,xslLocation,removeStarterTemplate);
}
}
/**
* declare V3 and NHS namespaces at the top of an XSLT transform file
* @param xout
*/
protected void declareNamespaces(XSLOutputFile xout)
{
xout.topOut().setAttribute("xmlns:" + NHSPREFIX, NHSURI);
xout.topOut().setAttribute("xmlns:" + AbstractMapperWrapper.V3PREFIX, AbstractMapperWrapper.V3NAMESPACEURI);
}
/**
* make a standalone wrapper XSLT file at the specified file location
* @param xout an empty xsl output file which has been created, and will be written to a location
* after the end of this call
* @param isInWrapper - true if it is to be an in-wrapper transform, false for an out-wrapper transform
*/
public void makeHeaderForStandaloneWrapperXSLT(XSLOutputFile xout, boolean isInWrapper) throws MapperException
{
// declare namespaces at the top of the xslt
declareNamespaces(xout);
/* the standalone NHS wrapper out-transform should be in a file,
* in the same folder as the mapper file. */
String xslLocation = FileUtil.getXSLLocation(wrapperXSLFileName(isInWrapper),ms());
boolean removeStarterTemplate= false;
addTemplatesFromFile(xout,xslLocation,removeStarterTemplate);
}
abstract String wrapperXSLFileName(boolean isIn);
protected void trace(String s) {if (tracing()) System.out.println(s);}
}