package com.openMap1.mapper.converters; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.Vector; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.structures.MapperWrapper; import com.openMap1.mapper.util.XMLUtil; import com.openMap1.mapper.util.XSLOutputFile; import com.openMap1.mapper.writer.TemplateFilter; import com.openMap1.mapper.MappedStructure; /** * Wrapper class which converts any ASTM CCR message into a form * which is more convenient for mapping, or does the reverse 'out' * transform. * * @author robert * */ public class CCRConverter extends AbstractMapperWrapper implements MapperWrapper { protected boolean tracing() {return false;} private static String CCR_ROOT = "ContinuityOfCareRecord"; /** * key = Actor id string * element: keys are roles played by that actor; element "1" */ private Hashtable<String,Hashtable<String,String>> actorRoles; // defined actor roles private static String PATIENT = "patient"; private static String HCP = "hcp"; private static String SUPPORT = "support"; private static String FAMILY = "family"; private static String HCP_ORGANISATION = "hcpOrganisation"; private static String INSURER = "insurer"; private static String SUPPLIER = "supplier"; private static String DOCUMENT_URL = "documentURL"; private static String IT_SYSTEM = "ITSystem"; // ends of XPaths which imply that an actor (whose ID is at that location) plays a role private String[][] rolePaths = { {PATIENT, "/Patient/ActorID"}, {INSURER, "/PaymentProvider/ActorID"}, {SUPPORT, "/SupportProvider/ActorID"}, {FAMILY, "/FamilyMember/ActorID"}, {DOCUMENT_URL, "/ReferenceID"}, {SUPPLIER, "/Manufacturer/ActorID"}, {HCP, "/HealthCareProviders/Provider/ActorID"}, {HCP_ORGANISATION, "/Encounter/Locations/Location/Actor/ActorID"}, {IT_SYSTEM, "/Source/Actor/ActorID"}, }; /** * @param ms set of mappings which uses this wrapper transform * @param spare spare argument, set to name of topElementDef */ public CCRConverter(MappedStructure ms, Object spare) throws MapperException { super(ms,spare); } /** * @return the type of document transformed to and from; * see static constants in class AbstractMapperWrapper. */ public int transformType() {return AbstractMapperWrapper.XML_TYPE;} /** * @return the file extension of the outer document */ public String fileExtension() {return "*.xml";} //-------------------------------------------------------------------------------------------- // Transform methods in the MapperWrapper Interface //-------------------------------------------------------------------------------------------- /** * * @param incoming; must be of class Element or InputStream * @return the result of the in wrapper transform */ public Document transformIn(Object CCRRootObj) throws MapperException { // preliminaries if (!(CCRRootObj instanceof Element)) throw new MapperException("CCR root is not an Element"); Element CCRRoot = (Element)CCRRootObj; if (!(CCR_ROOT.equals(XMLUtil.getLocalName(CCRRoot)))) throw new MapperException("Root element name is not '" + CCR_ROOT + "'"); String CCRRootPath = "/" + CCR_ROOT; inResultDoc = XMLUtil.makeOutDoc(); // pre-scan to find the roles played by actors actorRoles = new Hashtable<String,Hashtable<String,String>>(); scanDocument(CCRRoot, CCRRootPath, AbstractMapperWrapper.IN_PRE_SCAN); // vanilla copy unless overridden for specific paths Element inRoot = scanDocument(CCRRoot, CCRRootPath, AbstractMapperWrapper.IN_TRANSFORM); inResultDoc.appendChild(inRoot); return inResultDoc; } /** * @param outgoing the root element produced by the XMLWriter when * writing out from a class model instance (seen through an objectGetter) * @return the result of the out wrapper transform; * must be of class Document or OutputStream */ public Object transformOut(Element outgoing) throws MapperException { if (!(CCR_ROOT.equals(XMLUtil.getLocalName(outgoing)))) throw new MapperException("Root element name of 'in' file is not '" + CCR_ROOT + "'"); String CCRRootPath = "/" + CCR_ROOT; outResultDoc = XMLUtil.makeOutDoc(); // vanilla copy unless overridden for specific paths Element outRoot = scanDocument(outgoing, CCRRootPath, AbstractMapperWrapper.OUT_TRANSFORM); outResultDoc.appendChild(outRoot); return outResultDoc; } //-------------------------------------------------------------------------------------------- // Pre-scan before In Transform //-------------------------------------------------------------------------------------------- /** * action for each node in the pre-scan to find out which actor ids play which roles */ protected Element inPreScanNode(Element el, String path) throws MapperException { recordRole(el,path); return null; } /** * if the path to an element is associated with a role, * record that the actor with id on the element plays the role * @param el an element * @param path the path to the element */ private void recordRole(Element el, String path) { String id = XMLUtil.getText(el); for (int p = 0; p < rolePaths.length; p++) { String[] rp = rolePaths[p]; // [0] = role; [1] = path end if (path.endsWith(rp[1])) addRole(id, rp[0], path); } } private void addRole(String id, String role, String path) { Hashtable<String,String> roles = actorRoles.get(id); if (roles == null) roles = new Hashtable<String,String>(); roles.put(role,"1"); actorRoles.put(id,roles); //trace("id '" + id + "'; role " + role + " at path " + path); } //-------------------------------------------------------------------------------------------- // Special methods for special XPaths in the In Transform //-------------------------------------------------------------------------------------------- /** * copy for each node in the in transform * super.inTransformNode makes a vanilla copy, * which this method overrides for specific XPaths */ protected Element inTransformNode(Element el, String path) throws MapperException { // attach role attributes to <Actor> elements and repeat any actor with more than one role if (path.equals("/ContinuityOfCareRecord/Actors")) { Element actorsInEl = XMLUtil.newElement(inResultDoc, "Actors"); Vector<Element>actors = XMLUtil.namedChildElements(el, "Actor"); String newPath = path + "/Actor"; for (Iterator<Element> ia = actors.iterator();ia.hasNext();) { Element actorOut = ia.next(); Element idEl = XMLUtil.firstNamedChild(actorOut,"ActorObjectID"); if (idEl != null) // it never should be null; all actors should have ids { String id = XMLUtil.getText(idEl); // repeat each <Actor> child for all of its roles Hashtable<String,String> roles = actorRoles.get(id); // an actor with no roles identified elsewhere in the document does not get copied across if (roles != null) for (Enumeration<String> en = roles.keys(); en.hasMoreElements();) { String role = en.nextElement(); // copy the tree below the <Actor>, maybe making changes Element actorIn = scanDocument(actorOut, newPath, AbstractMapperWrapper.IN_TRANSFORM); actorIn.setAttribute("role", role); actorsInEl.appendChild(actorIn); } } } return actorsInEl; } // if one of an actor's multiple IDs is the same as its ActorObjectId, change the tag name to ID_C else if (path.equals("/ContinuityOfCareRecord/Actors/Actor/IDs")) { Element result = null; Element idChild = XMLUtil.firstNamedChild(el, "ID"); if (idChild != null) //it never should be { String oneId = XMLUtil.getText(idChild); if (actorRoles.get(oneId) != null) result = XMLUtil.newElement(inResultDoc, "ID_C"); else result = XMLUtil.newElement(inResultDoc, "IDs"); /* deep import all its children; * no further change below the node with changed name. */ for (Iterator<Element> it = XMLUtil.childElements(el).iterator();it.hasNext();) result.appendChild(inResultDoc.importNode(it.next(), true)); } return result; } // add elements <CauseOfDeath>No</CauseOfDeath> if someone is not dead else if (path.equals("/ContinuityOfCareRecord/Body/FamilyHistory/FamilyProblemHistory/FamilyMember/HealthStatus")) { trace("Health status"); Element result = super.inTransformNode(el, path); // deep copy ; no other changes below this node for (Iterator<Element> ie = XMLUtil.childElements(el).iterator();ie.hasNext();) result.appendChild(inResultDoc.importNode(ie.next(),true)); Element desc = XMLUtil.firstNamedChild(result, "Description"); if (desc != null) { String description = XMLUtil.getText(XMLUtil.firstNamedChild(desc, "Text")); trace(description); if (!(("Deceased").equals(description))) { Element newCause = XMLUtil.textElement(inResultDoc, "CauseOfDeath","No"); result.appendChild(newCause); } } return result; } // convert descriptions of family relationships to SNOMED controlled terms else if (path.equals("/ContinuityOfCareRecord/Body/FamilyHistory/FamilyProblemHistory/FamilyMember/ActorRole/Text")) { String relation = XMLUtil.getText(el); String snomedRelation = relation; if (relation.equalsIgnoreCase("mother")) snomedRelation = "Biological mother"; if (relation.equalsIgnoreCase("father")) snomedRelation = "Biological father"; return XMLUtil.textElement(inResultDoc, "Text", snomedRelation); } // other paths - vanilla copy else return super.inTransformNode(el, path); } //-------------------------------------------------------------------------------------------- // Special methods for special XPaths in the Out Transform //-------------------------------------------------------------------------------------------- /** * copy for each node in the out transform * super.inTransformNode makes a vanilla copy, * which this method overrides for specific XPaths */ protected Element outTransformNode(Element el, String path) throws MapperException { // remove role attributes from <Actor> elements and do not repeat any actor with the same id if (path.equals("/ContinuityOfCareRecord/Actors")) { Element actorsOutEl = XMLUtil.newElement(outResultDoc, "Actors"); Vector<Element>actors = XMLUtil.namedChildElements(el, "Actor"); String newPath = path + "/Actor"; // record all actor ids that have been written out Hashtable<String,String> writtenIds = new Hashtable<String,String>(); for (Iterator<Element> ia = actors.iterator();ia.hasNext();) { Element actorIn = ia.next(); Element idEl = XMLUtil.firstNamedChild(actorIn,"ActorObjectID"); if (idEl != null) // it never should be null; all actors should have ids { String id = XMLUtil.getText(idEl); // if the actor with this id has not been written out already.... if (writtenIds.get(id) == null) { Element actorOut = scanDocument(actorIn, newPath, AbstractMapperWrapper.OUT_TRANSFORM); actorOut.removeAttribute("role"); actorsOutEl.appendChild(actorOut); writtenIds.put(id,"1"); } } } return actorsOutEl; } /* if one of an actor's multiple ids was the same as its ActorObjectId, * so the tag name was changed to ID_C, change the tag name back again. */ else if (path.equals("/ContinuityOfCareRecord/Actors/Actor/ID_C")) { Element result = XMLUtil.newElement(outResultDoc, "IDs"); /* deep import all its children; * no further change below that node. */ for (Iterator<Element> it = XMLUtil.childElements(el).iterator();it.hasNext();) result.appendChild(outResultDoc.importNode(it.next(), true)); return result; } // remove elements <CauseOfDeath>No</CauseOfDeath> if someone is not dead else if (path.equals("/ContinuityOfCareRecord/Body/FamilyHistory/FamilyProblemHistory/FamilyMember/HealthStatus")) { Element result = super.outTransformNode(el, path); // deep copy ; not other changes below this node for (Iterator<Element> ie = XMLUtil.childElements(el).iterator();ie.hasNext();) result.appendChild(outResultDoc.importNode(ie.next(),true)); Element desc = XMLUtil.firstNamedChild(result, "Description"); if (desc != null) { String description = XMLUtil.getText(XMLUtil.firstNamedChild(desc, "Text")); if (!(("Deceased").equals(description))) { Element cause = XMLUtil.firstNamedChild(result, "CauseOfDeath"); if (cause != null) result.removeChild(cause); } } return result; } // descriptions of family relationships converted to SNOMED controlled terms - no conversion back needed // else if (path.equals("/ContinuityOfCareRecord/Body/FamilyHistory/FamilyProblemHistory/FamilyMember/ActorRole/Text")) // other paths - vanilla copy else return super.outTransformNode(el, path); } //---------------------------------------------------------------------------------------- // Sample property conversion methods //---------------------------------------------------------------------------------------- private static String[][] frequencyConverter = { {"Daily","24"}, {"BID","12"}, {"QID","6"} }; /** * Conversion from a frequency per day to an hours interval * @param ht * @param frequency * @return */ public static String frequencyToHoursInterval(Hashtable<?,?> ht, String frequency) { String interval = frequency; // default if lookup fails for (int i = 0; i < frequencyConverter.length; i++) { String[] pair = frequencyConverter[i]; if (pair[0].equals(frequency)) interval = pair[1]; } return interval; } /** * Conversion from an interval in hours to a daily frequency * @param ht * @param interval * @return */ public static String hoursIntervalToFrequency(Hashtable<?,?> ht, String interval) { String frequency = interval; // default if lookup fails for (int i = 0; i < frequencyConverter.length; i++) { String[] pair = frequencyConverter[i]; if (pair[1].equals(interval)) frequency = pair[0]; } return frequency; } //---------------------------------------------------------------------------------------- // XSLT versions of wrapper transforms, for inclusion in full XSLT transforms //---------------------------------------------------------------------------------------- /** * @param xgen the XSL generator writing transformation templates * 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 = "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) throws MapperException { /* the standalone CCR wrapper in-transform should be in a file CCRInWrapper.xsl, * in the same folder as the CCR mapper file. */ boolean isInWrapper = true; String xslLocation = templateFilter.getXSLLocation("CCRInWrapper.xsl",isInWrapper); boolean removerStarterTemplate = true; addTemplatesFromFile(xout,xslLocation, removerStarterTemplate); } /** * @param xout the XSL output file * @param templateFilter has a boolean method to say if a template should be included * 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 { /* the standalone CCR wrapper out-transform should be in a file CCROutWrapper.xsl, * in the same folder as the CCR mapper file. */ boolean isInWrapper = false; String xslLocation = templateFilter.getXSLLocation("CCROutWrapper.xsl",isInWrapper); // declare the namespace prefix 'ccr' (used in the wrapper xsl) at the top level of the overall XSL xout.topOut().setAttribute("xmlns:ccr", "urn:astm-org:CCR"); boolean removeStarterTemplate = true; addTemplatesFromFile(xout,xslLocation,removeStarterTemplate); } }