001package org.hl7.fhir.convertors.misc.ccda; 002 003 004 005 006 007/* 008 Copyright (c) 2011+, HL7, Inc. 009 All rights reserved. 010 011 Redistribution and use in source and binary forms, with or without modification, 012 are permitted provided that the following conditions are met: 013 014 * Redistributions of source code must retain the above copyright notice, this 015 list of conditions and the following disclaimer. 016 * Redistributions in binary form must reproduce the above copyright notice, 017 this list of conditions and the following disclaimer in the documentation 018 and/or other materials provided with the distribution. 019 * Neither the name of HL7 nor the names of its contributors may be used to 020 endorse or promote products derived from this software without specific 021 prior written permission. 022 023 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 024 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 025 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 026 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 027 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 028 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 029 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 030 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 031 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 032 POSSIBILITY OF SUCH DAMAGE. 033 034 */ 035 036 037import org.fhir.ucum.UcumService; 038import org.hl7.fhir.convertors.misc.CDAUtilities; 039import org.hl7.fhir.convertors.misc.Convert; 040import org.hl7.fhir.dstu3.context.IWorkerContext; 041import org.hl7.fhir.dstu3.model.*; 042import org.hl7.fhir.dstu3.model.AllergyIntolerance.*; 043import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; 044import org.hl7.fhir.dstu3.model.Composition.CompositionAttestationMode; 045import org.hl7.fhir.dstu3.model.Composition.CompositionAttesterComponent; 046import org.hl7.fhir.dstu3.model.Composition.DocumentConfidentiality; 047import org.hl7.fhir.dstu3.model.Composition.SectionComponent; 048import org.hl7.fhir.dstu3.model.ListResource.ListEntryComponent; 049import org.hl7.fhir.dstu3.model.Narrative.NarrativeStatus; 050import org.hl7.fhir.dstu3.model.Observation.ObservationRelatedComponent; 051import org.hl7.fhir.dstu3.model.Observation.ObservationRelationshipType; 052import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; 053import org.hl7.fhir.dstu3.model.Procedure.ProcedurePerformerComponent; 054import org.hl7.fhir.dstu3.utils.NarrativeGenerator; 055import org.hl7.fhir.dstu3.utils.ToolingExtensions; 056import org.w3c.dom.Element; 057 058import java.io.InputStream; 059import java.util.HashMap; 060import java.util.List; 061import java.util.Map; 062import java.util.UUID; 063 064/** 065 * Advance Directives Section 42348-3 : 066 * Allergies, Adverse Reactions, Alerts Section 48765-2 : List(AlleryIntolerance) processAdverseReactionsSection 067 * Anesthesia Section 59774-0 : 068 * Assessment Section 51848-0 : 069 * Assessment and Plan Section 51487-2 : 070 * Chief Complaint Section 10154-3 : 071 * Chief Complaint and Reason for Visit Section 46239-0 : 072 * Complications 55109-3: 073 * DICOM Object Catalog Section - DCM 121181 : 074 * Discharge Diet Section 42344-2 : 075 * Encounters Section 46240-8: 076 * Family History Section 10157-6 : 077 * Findings Section 18782-3 : 078 * Functional Status Section 47420-5 : 079 * General Status Section 10210-3 : 080 * History of Past Illness Section 11348-0 : 081 * History of Present Illness Section 10164-2 : 082 * Hospital Admission Diagnosis Section 46241-6 : 083 * Hospital Consultations Section 18841-7 : 084 * Hospital Course Section 8648-8 : 085 * Hospital Discharge Diagnosis Section 11535-2 : 086 * Hospital Discharge Instructions Section : 087 * Hospital Discharge Medications Section (entries optional) 10183-2 : 088 * Hospital Discharge Physical Section 10184-0 : 089 * Hospital Discharge Studies Summary Section 11493-4 : 090 * Immunizations Section 11369-6 : 091 * Interventions Section 62387-6 : 092 * Medical Equipment Section 46264-8 : 093 * Medical (General) History Section 11329-0 : 094 * Medications Section 10160-0 : 095 * Medications Administered Section 29549-3 : 096 * Objective Section 61149-1 : 097 * Operative Note Fluid Section 10216-0 : 098 * Operative Note Surgical Procedure Section 10223-6 : 099 * Payers Section 48768-6 : 100 * Physical Exam Section 29545-1 : 101 * Plan of Care Section 18776-5 : 102 * Planned Procedure Section 59772-: 103 * Postoperative Diagnosis Section 10218-6 : 104 * Postprocedure Diagnosis Section 59769-0 : 105 * Preoperative Diagnosis Section 10219-4 : 106 * Problem Section 11450-4 : 107 * Procedure Description Section 29554-3: 108 * Procedure Disposition Section 59775-7 : 109 * Procedure Estimated Blood Loss Section 59770-8 : 110 * Procedure Findings Section 59776-5 : 111 * Procedure Implants Section 59771-6 : 112 * Procedure Indications Section 59768-2 : 113 * Procedure Specimens Taken Section 59773-2 : 114 * Procedures Section 47519-4 : List (Procedure) processProceduresSection 115 * Reason for Referral Section 42349-1 : 116 * Reason for Visit Section 29299-5 : 117 * Results Section 30954-2 : 118 * Review of Systems Section 10187-3 : 119 * Social History Section 29762-2 : List (Observation) processSocialHistorySection 120 * Subjective Section 61150-9: 121 * Surgical Drains Section 11537-8 : 122 * Vital Signs Section 8716-3 : List(Observation) processVitalSignsSection 123 * <p> 124 * <p> 125 * MU Sections: 126 * Allergies/Adverse Reactions 127 * Problems 128 * Encounters 129 * Medications 130 * Results 131 * Vital Signs 132 * Procedures 133 * Immunizations 134 * Reason for Referral 135 * Hospital Discharge Instructions 136 * Functional Status 137 * Plan of Care 138 * Hospital Discharge Medication 139 * All of General Header 140 * 141 * @author Grahame 142 */ 143public class CCDAConverter { 144 145 protected CDAUtilities cda; 146 protected Element doc; 147 protected Convert convert; 148 protected Bundle feed; 149 protected Composition composition; 150 protected Map<String, Practitioner> practitionerCache = new HashMap<String, Practitioner>(); 151 protected Integer refCounter = 0; 152 protected UcumService ucumSvc; 153 protected IWorkerContext context; 154 155 public CCDAConverter(UcumService ucumSvc, IWorkerContext context) { 156 super(); 157 this.ucumSvc = ucumSvc; 158 this.context = context; 159 } 160 161 public Bundle convert(InputStream stream) throws Exception { 162 163 cda = new CDAUtilities(stream); 164 doc = cda.getElement(); 165 cda.checkTemplateId(doc, "2.16.840.1.113883.10.20.22.1.1"); 166 convert = new Convert(cda, ucumSvc, "Z"); 167 168 // check it's a CDA/CCD 169 feed = new Bundle(); 170 feed.setMeta(new Meta().setLastUpdatedElement(InstantType.now())); 171 feed.setId(makeUUIDReference()); 172 feed.getMeta().getTag().add(new Coding()); // todo-bundle ("http://hl7.org/fhir/tag", "http://hl7.org/fhir/tag/document", "Document")); 173 174 // process the header 175 makeDocument(); 176 composition.setSubject(Factory.makeReference(makeSubject())); 177 for (Element e : cda.getChildren(doc, "author")) 178 composition.getAuthor().add(Factory.makeReference(makeAuthor(e))); 179 // todo: data enterer & informant goes in provenance 180 composition.setCustodian(Factory.makeReference(makeOrganization( 181 cda.getDescendent(doc, "custodian/assignedCustodian/representedCustodianOrganization"), "Custodian"))); 182 // todo: informationRecipient 183 for (Element e : cda.getChildren(doc, "legalAuthenticator")) 184 composition.getAttester().add(makeAttester(e, CompositionAttestationMode.LEGAL, "Legal Authenticator")); 185 for (Element e : cda.getChildren(doc, "authenticator")) 186 composition.getAttester().add(makeAttester(e, CompositionAttestationMode.PROFESSIONAL, "Authenticator")); 187 188 // process the contents 189 // we do this by section - keep the original section order 190 Element body = cda.getDescendent(doc, "component/structuredBody"); 191 processComponentSections(composition.getSection(), body); 192 return feed; 193 } 194 195 protected String addReference(DomainResource r, String title, String id) throws Exception { 196 if (r.getText() == null) 197 r.setText(new Narrative()); 198 if (r.getText().getDiv() == null) { 199 r.getText().setStatus(NarrativeStatus.GENERATED); 200 new NarrativeGenerator("", "", context).generate(r); 201 } 202 r.setMeta(new Meta().setLastUpdatedElement(InstantType.now())); 203 r.setId(id); 204 feed.getEntry().add(new BundleEntryComponent().setResource(r)); 205 return id; 206 } 207 208 protected void makeDocument() throws Exception { 209 composition = (Composition) ResourceFactory.createResource("Composition"); 210 addReference(composition, "Composition", makeUUIDReference()); 211 212 Element title = cda.getChild(doc, "title"); 213 composition.setTitle(title.getTextContent()); 214 215 if (cda.getChild(doc, "setId") != null) { 216 feed.setId(convert.makeURIfromII(cda.getChild(doc, "id"))); 217 composition.setIdentifier(convert.makeIdentifierFromII(cda.getChild(doc, "setId"))); 218 } else 219 composition.setIdentifier(convert.makeIdentifierFromII(cda.getChild(doc, "id"))); // well, we fall back to id 220 221 composition.setDateElement(convert.makeDateTimeFromTS(cda.getChild(doc, "effectiveTime"))); 222 composition.setType(convert.makeCodeableConceptFromCD(cda.getChild(doc, "code"))); 223 composition.setConfidentiality(convertConfidentiality(cda.getChild(doc, "confidentialityCode"))); 224 if (cda.getChild(doc, "confidentialityCode") != null) 225 composition.setLanguage(cda.getChild(doc, "confidentialityCode").getAttribute("value")); // todo - fix streaming for this 226 227 Element ee = cda.getChild(doc, "componentOf"); 228 if (ee != null) 229 ee = cda.getChild(ee, "encompassingEncounter"); 230 if (ee != null) { 231 Encounter visit = new Encounter(); 232 for (Element e : cda.getChildren(ee, "id")) 233 visit.getIdentifier().add(convert.makeIdentifierFromII(e)); 234 visit.setPeriod(convert.makePeriodFromIVL(cda.getChild(ee, "effectiveTime"))); 235 composition.getEvent().add(new Composition.CompositionEventComponent()); 236 composition.getEvent().get(0).getCode().add(convert.makeCodeableConceptFromCD(cda.getChild(ee, "code"))); 237 composition.getEvent().get(0).setPeriod(visit.getPeriod()); 238 composition.getEvent().get(0).getDetail().add(Factory.makeReference(addReference(visit, "Encounter", makeUUIDReference()))); 239 } 240 241 // main todo: fill out the narrative, but before we can do that, we have to convert everything else 242 } 243 244 protected DocumentConfidentiality convertConfidentiality(Element child) throws org.hl7.fhir.exceptions.FHIRException { 245 // TODO Auto-generated method stub 246 return DocumentConfidentiality.fromCode(child.getAttribute("code")); 247 } 248 249 protected String makeSubject() throws Exception { 250 Element rt = cda.getChild(doc, "recordTarget"); 251 Element pr = cda.getChild(rt, "patientRole"); 252 Element p = cda.getChild(pr, "patient"); 253 254 Patient pat = (Patient) ResourceFactory.createResource("Patient"); 255 for (Element e : cda.getChildren(pr, "id")) 256 pat.getIdentifier().add(convert.makeIdentifierFromII(e)); 257 258 for (Element e : cda.getChildren(pr, "addr")) 259 pat.getAddress().add(convert.makeAddressFromAD(e)); 260 for (Element e : cda.getChildren(pr, "telecom")) 261 pat.getTelecom().add(convert.makeContactFromTEL(e)); 262 for (Element e : cda.getChildren(p, "name")) 263 pat.getName().add(convert.makeNameFromEN(e)); 264 pat.setGender(convert.makeGenderFromCD(cda.getChild(p, "administrativeGenderCode"))); 265 pat.setBirthDateElement(convert.makeDateFromTS(cda.getChild(p, "birthTime"))); 266 pat.setMaritalStatus(convert.makeCodeableConceptFromCD(cda.getChild(p, "maritalStatusCode"))); 267 pat.getExtension().add(Factory.newExtension(CcdaExtensions.NAME_RELIGION, convert.makeCodeableConceptFromCD(cda.getChild(p, "religiousAffiliationCode")), false)); 268 pat.getExtension().add(Factory.newExtension(CcdaExtensions.DAF_NAME_RACE, convert.makeCodeableConceptFromCD(cda.getChild(p, "raceCode")), false)); 269 pat.getExtension().add(Factory.newExtension(CcdaExtensions.DAF_NAME_ETHNICITY, convert.makeCodeableConceptFromCD(cda.getChild(p, "ethnicGroupCode")), false)); 270 pat.getExtension().add(Factory.newExtension(CcdaExtensions.NAME_BIRTHPLACE, convert.makeAddressFromAD(cda.getChild(p, new String[]{"birthplace", "place", "addr"})), false)); 271 272 Patient.ContactComponent guardian = new Patient.ContactComponent(); 273 pat.getContact().add(guardian); 274 guardian.getRelationship().add(Factory.newCodeableConcept("GUARD", "urn:oid:2.16.840.1.113883.5.110", "guardian")); 275 Element g = cda.getChild(p, "guardian"); 276 for (Element e : cda.getChildren(g, "addr")) 277 if (guardian.getAddress() == null) 278 guardian.setAddress(convert.makeAddressFromAD(e)); 279 for (Element e : cda.getChildren(g, "telecom")) 280 guardian.getTelecom().add(convert.makeContactFromTEL(e)); 281 g = cda.getChild(g, "guardianPerson"); 282 for (Element e : cda.getChildren(g, "name")) 283 if (guardian.getName() == null) 284 guardian.setName(convert.makeNameFromEN(e)); 285 286 Element l = cda.getChild(p, "languageCommunication"); 287 CodeableConcept cc = new CodeableConcept(); 288 Coding c = new Coding(); 289 c.setCode(cda.getChild(l, "languageCode").getAttribute("code")); 290 cc.getCoding().add(c); 291 pat.addCommunication().setLanguage(cc); 292 293 // todo: this got broken.... lang.setMode(convert.makeCodeableConceptFromCD(cda.getChild(l, "modeCode"))); 294 cc.getExtension().add(Factory.newExtension(CcdaExtensions.NAME_LANG_PROF, convert.makeCodeableConceptFromCD(cda.getChild(l, "modeCode")), false)); 295 pat.getExtension().add(Factory.newExtension(CcdaExtensions.NAME_RELIGION, convert.makeCodeableConceptFromCD(cda.getChild(p, "religiousAffiliationCode")), false)); 296 pat.setManagingOrganization(Factory.makeReference(makeOrganization(cda.getChild(pr, "providerOrganization"), "Provider"))); 297 return addReference(pat, "Subject", makeUUIDReference()); 298 } 299 300 protected String makeOrganization(Element org, String name) throws Exception { 301 Organization o = new Organization(); 302 for (Element e : cda.getChildren(org, "id")) 303 o.getIdentifier().add(convert.makeIdentifierFromII(e)); 304 for (Element e : cda.getChildren(org, "name")) 305 o.setName(e.getTextContent()); 306 for (Element e : cda.getChildren(org, "addr")) 307 o.getAddress().add(convert.makeAddressFromAD(e)); 308 for (Element e : cda.getChildren(org, "telecom")) 309 o.getTelecom().add(convert.makeContactFromTEL(e)); 310 311 return addReference(o, name, makeUUIDReference()); 312 } 313 314 protected String makeAuthor(Element auth) throws Exception { 315 Element aa = cda.getChild(auth, "assignedAuthor"); 316 Element ap = cda.getChild(aa, "assignedPerson"); 317 318 Practitioner pr = (Practitioner) ResourceFactory.createResource("Practitioner"); 319 for (Element e : cda.getChildren(aa, "id")) 320 pr.getIdentifier().add(convert.makeIdentifierFromII(e)); 321 for (Element e : cda.getChildren(aa, "addr")) 322 if (pr.getAddress() == null) 323 pr.getAddress().add(convert.makeAddressFromAD(e)); 324 for (Element e : cda.getChildren(aa, "telecom")) 325 pr.getTelecom().add(convert.makeContactFromTEL(e)); 326 for (Element e : cda.getChildren(ap, "name")) 327 if (pr.getName() != null) 328 pr.addName(convert.makeNameFromEN(e)); 329 330 return addReference(pr, "Author", makeUUIDReference()); 331 } 332 333 protected String makeUUIDReference() { 334 return "urn:uuid:" + UUID.randomUUID().toString().toLowerCase(); 335 } 336 337 protected CompositionAttesterComponent makeAttester(Element a1, CompositionAttestationMode mode, String title) throws Exception { 338 Practitioner pr = (Practitioner) ResourceFactory.createResource("Practitioner"); 339 Element ass = cda.getChild(a1, "assignedEntity"); 340 for (Element e : cda.getChildren(ass, "id")) 341 pr.getIdentifier().add(convert.makeIdentifierFromII(e)); 342 for (Element e : cda.getChildren(ass, "addr")) 343 if (pr.getAddress() == null) // just take the first 344 pr.getAddress().add(convert.makeAddressFromAD(e)); 345 for (Element e : cda.getChildren(ass, "telecom")) 346 pr.getTelecom().add(convert.makeContactFromTEL(e)); 347 Element ap = cda.getChild(ass, "assignedPerson"); 348 for (Element e : cda.getChildren(ap, "name")) 349 if (pr.getName() == null) // just take the first 350 pr.addName(convert.makeNameFromEN(e)); 351 352 353 CompositionAttesterComponent att = new CompositionAttesterComponent(); 354 att.addMode(mode); 355 att.setTimeElement(convert.makeDateTimeFromTS(cda.getChild(a1, "time"))); 356 att.setParty(Factory.makeReference(addReference(pr, title, makeUUIDReference()))); 357 return att; 358 } 359 360 protected void processComponentSections(List<SectionComponent> sections, Element container) throws Exception { 361 for (Element c : cda.getChildren(container, "component")) { 362 SectionComponent s = processSection(cda.getChild(c, "section")); 363 if (s != null) 364 sections.add(s); 365 } 366 367 } 368 369 protected SectionComponent processSection(Element section) throws Exception { 370 checkNoSubject(section, "Section"); 371 // this we do by templateId 372 if (cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.6") || cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.6.1")) 373 return processAdverseReactionsSection(section); 374 else if (cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.7") || cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.7.1")) 375 return processProceduresSection(section); 376 else if (cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.17")) 377 return processSocialHistorySection(section); 378 else if (cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.4") || cda.hasTemplateId(section, "2.16.840.1.113883.10.20.22.2.4.1")) 379 return processVitalSignsSection(section); 380 else 381 // todo: error? 382 return null; 383 } 384 385 protected void checkNoSubject(Element act, String path) throws Exception { 386 if (cda.getChild(act, "subject") != null) 387 throw new Exception("The conversion program cannot accept a nullFlavor at the location " + path); 388 } 389 390 protected SectionComponent processProceduresSection(Element section) throws Exception { 391 ListResource list = new ListResource(); 392 for (Element entry : cda.getChildren(section, "entry")) { 393 Element procedure = cda.getlastChild(entry); 394 395 if (cda.hasTemplateId(procedure, "2.16.840.1.113883.10.20.22.4.14")) { 396 processProcedure(list, procedure, ProcedureType.Procedure); 397 } else if (cda.hasTemplateId(procedure, "2.16.840.1.113883.10.20.22.4.13")) { 398 processProcedure(list, procedure, ProcedureType.Observation); 399 } else if (cda.hasTemplateId(procedure, "2.16.840.1.113883.10.20.22.4.12")) { 400 processProcedure(list, procedure, ProcedureType.Act); 401 } else 402 throw new Exception("Unhandled Section template ids: " + cda.showTemplateIds(procedure)); 403 } 404 405 // todo: text 406 SectionComponent s = new Composition.SectionComponent(); 407 s.setCode(convert.makeCodeableConceptFromCD(cda.getChild(section, "code"))); 408 // todo: check subject 409 s.addEntry(Factory.makeReference(addReference(list, "Procedures", makeUUIDReference()))); 410 return s; 411 412 } 413 414 protected void processProcedure(ListResource list, Element procedure, ProcedureType type) throws Exception { 415 switch (type) { 416 case Procedure: 417 cda.checkTemplateId(procedure, "2.16.840.1.113883.10.20.22.4.14"); 418 break; 419 case Observation: 420 cda.checkTemplateId(procedure, "2.16.840.1.113883.10.20.22.4.13"); 421 break; 422 case Act: 423 cda.checkTemplateId(procedure, "2.16.840.1.113883.10.20.22.4.12"); 424 } 425 checkNoNegationOrNullFlavor(procedure, "Procedure (" + type + ")"); 426 checkNoSubject(procedure, "Procedure (" + type + ")"); 427 428 Procedure p = new Procedure(); 429 addItemToList(list, p); 430 431 // moodCode is either INT or EVN. INT is not handled yet. INT is deprecated anyway 432 if (procedure.getAttribute("moodCode").equals("INT")) 433 p.getModifierExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-planned", Factory.newBoolean(true), false)); 434 435 // SHALL contain at least one [1..*] id (CONF:7655). 436 for (Element e : cda.getChildren(procedure, "id")) 437 p.getIdentifier().add(convert.makeIdentifierFromII(e)); 438 439 // SHALL contain exactly one [1..1] code (CONF:7656). 440 // This code @code in a procedure activity SHOULD be selected from LOINC or SNOMED CT and MAY be selected from CPT-4, ICD9 Procedures, ICD10 Procedures 441 p.setCode(convert.makeCodeableConceptFromCD(cda.getChild(procedure, "code"))); 442 443 // SHALL contain exactly one [1..1] statusCode/@code, which SHALL be selected from ValueSet 2.16.840.1.113883.11.20.9.22 ProcedureAct 444 // completed | active | aborted | cancelled - not in FHIR 445 p.getModifierExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-status", Factory.newCode(cda.getStatus(procedure)), false)); 446 447 // SHOULD contain zero or one [0..1] effectiveTime (CONF:7662). 448 p.setPerformed(convert.makePeriodFromIVL(cda.getChild(procedure, "effectiveTime"))); 449 450 // MAY contain zero or one [0..1] priorityCode/@code, which SHALL be selected from ValueSet 2.16.840.1.113883.1.11.16866 ActPriority DYNAMIC (CONF:7668) 451 p.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-priority", convert.makeCodeableConceptFromCD(cda.getChild(procedure, "priorityCode")), false)); 452 453 // MAY contain zero or one [0..1] methodCode (CONF:7670). 454 p.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-method", convert.makeCodeableConceptFromCD(cda.getChild(procedure, "methodCode")), false)); 455 456 if (type == ProcedureType.Observation) { 457 // for Procedure-Observation: 458 // 9. SHALL contain exactly one [1..1] value (CONF:16846). 459 // don't know what this is. It's not the actual result of the procedure (that goes in results "This section records ... procedure observations"), and there seems to be no value. The example as <value xsi:type="CD"/> which is not valid 460 // so we ignore this for now 461 } 462 463 // SHOULD contain zero or more [0..*] targetSiteCode/@code, which SHALL be selected from ValueSet 2.16.840.1.113883.3.88.12.3221.8.9 Body site DYNAMIC (CONF:7683). 464 for (Element e : cda.getChildren(procedure, "targetSiteCode")) 465 p.addBodySite(convert.makeCodeableConceptFromCD(e)); 466 467 // MAY contain zero or more [0..*] specimen (CONF:7697). 468 // todo: add these as extensions when specimens are done. 469 470 // SHOULD contain zero or more [0..*] performer (CONF:7718) such that it 471 for (Element e : cda.getChildren(procedure, "performer")) { 472 ProcedurePerformerComponent pp = new ProcedurePerformerComponent(); 473 p.getPerformer().add(pp); 474 pp.setActor(makeReferenceToPractitionerForAssignedEntity(e, p)); 475 } 476 477 for (Element participant : cda.getChildren(procedure, "participant")) { 478 Element participantRole = cda.getlastChild(participant); 479 if (type == ProcedureType.Procedure && cda.hasTemplateId(participantRole, "2.16.840.1.113883.10.20.22.4.37")) { 480 // MAY contain zero or more [0..*] participant (CONF:7751) such that it SHALL contain exactly one [1..1] @typeCode="DEV" Device 481 // implanted devices 482 p.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/implanted-devices", Factory.makeReference(processDevice(participantRole, p)), false)); 483 } else if (cda.hasTemplateId(participantRole, "2.16.840.1.113883.10.20.22.4.32")) { 484 // MAY contain zero or more [0..*] participant (CONF:7765) such that it SHALL contain exactly one [1..1] Service Delivery Location (templateId:2.16.840.1.113883.10.20.22.4.32) (CONF:7767) 485 p.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/location", Factory.makeReference(processSDLocation(participantRole, p)), false)); 486 } 487 } 488 489 for (Element e : cda.getChildren(procedure, "entryRelationship")) { 490 Element a /* act*/ = cda.getlastChild(e); 491 if (a.getLocalName().equals("encounter")) { 492 // MAY contain zero or more [0..*] entryRelationship (CONF:7768) such that it SHALL contain exactly one encounter which SHALL contain exactly one [1..1] id (CONF:7773). 493 // todo - and process as a full encounter while we're at it 494 } else if (cda.hasTemplateId(a, "2.16.840.1.113883.10.20.22.4.20")) { 495 // MAY contain zero or one [0..1] entryRelationship (CONF:7775) such that it SHALL contain exactly one [1..1] Instructions (templateId:2.16.840.1.113883.10.20.22.4.20) (CONF:7778). 496 // had code for type, plus text for instructions 497 Extension n = Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-instructions", null, true); 498 n.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-instructions-type", convert.makeCodeableConceptFromCD(cda.getChild(a, "code")), false)); 499 n.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/procedure-instructions-text", convert.makeStringFromED(cda.getChild(a, "text")), false)); 500 p.getExtension().add(n); 501 } else if (cda.hasTemplateId(a, "2.16.840.1.113883.10.20.22.4.19")) { 502 // MAY contain zero or more [0..*] entryRelationship (CONF:7779) such that it SHALL contain exactly one [1..1] Indication (templateId:2.16.840.1.113883.10.20.22.4.19) (CONF:7781). 503 p.addReasonCode(processIndication(a)); 504 } else if (cda.hasTemplateId(cda.getlastChild(e), "2.16.840.1.113883.10.20.22.4.16")) { 505 // MAY contain zero or one [0..1] entryRelationship (CONF:7886) such that it SHALL contain exactly one [1..1] Medication Activity (templateId:2.16.840.1.113883.10.20.22.4.16) (CONF:7888). 506 // todo 507 } 508 } 509 } 510 511 protected String processSDLocation(Element participantRole, DomainResource r) throws Exception { 512 Location l = new Location(); 513 l.setType(convert.makeCodeableConceptFromCD(cda.getChild(participantRole, "code"))); 514 for (Element id : cda.getChildren(participantRole, "id")) { 515 if (l.getIdentifier() == null) 516 l.getIdentifier().add(convert.makeIdentifierFromII(id)); 517 } 518 for (Element addr : cda.getChildren(participantRole, "addr")) { 519 if (l.getAddress() == null) 520 l.setAddress(convert.makeAddressFromAD(addr)); 521 } 522 523 for (Element telecom : cda.getChildren(participantRole, "telecom")) { 524 l.getTelecom().add(convert.makeContactFromTEL(telecom)); 525 } 526 527 528 Element place = cda.getChild(participantRole, "playingDevice"); 529 if (cda.getChild(place, "name") != null) 530 l.setName(cda.getChild(place, "name").getTextContent()); 531 532 String id = nextRef(); 533 l.setId(id); 534 r.getContained().add(l); 535 return "#" + id; 536 } 537 538 protected String processDevice(Element participantRole, DomainResource r) throws Exception { 539 Device d = new Device(); 540 for (Element id : cda.getChildren(participantRole, "id")) { 541 // todo: check for UDIs, how? 542 d.getIdentifier().add(convert.makeIdentifierFromII(id)); 543 } 544 Element device = cda.getChild(participantRole, "playingDevice"); 545 // todo: if (cda.getChild(device, "code") != null) 546 d.setType(convert.makeCodeableConceptFromCD(cda.getChild(device, "code"))); 547 548 // CCDA has an id - this is manufacturer? We just call it the name, but what to do about this? 549 Element org = cda.getChild(participantRole, "scopingEntity"); 550 d.setManufacturer(convert.makeURIfromII(cda.getChild(org, "id"))); 551 552 String id = nextRef(); 553 d.setId(id); 554 r.getContained().add(d); 555 return "#" + id; 556 } 557 558 protected CodeableConcept processIndication(Element obs) throws Exception { 559 Element v = cda.getChild(obs, "value"); 560 if (v == null) { 561 // have to find it by ID 562 Element o = cda.getById(cda.getChild(obs, "id"), "value"); 563 if (o != null) 564 v = cda.getChild(obs, "value"); 565 } 566 if (v != null) 567 return convert.makeCodeableConceptFromCD(v); 568 else 569 return null; 570 } 571 572 protected Reference makeReferenceToPractitionerForAssignedEntity(Element assignedEntity, DomainResource r) throws Exception { 573 574 Reference ref = null; 575 // do we have this by id? 576 String uri = getIdForEntity(assignedEntity); 577 Practitioner p = null; 578 if (uri != null) { 579 ref = Factory.makeReference(uri); 580 p = practitionerCache.get(uri); 581 } 582 if (p == null) { 583 p = new Practitioner(); 584 if (uri == null) { 585 // make a contained practitioner 586 String n = nextRef(); 587 p.setId(n); 588 r.getContained().add(p); 589 ref = Factory.makeReference("#" + n); 590 } else { 591 // add this to feed 592 ref = Factory.makeReference(addReference(p, "Practitioner", uri)); 593 } 594 } 595 // ref and p are both sorted. now we fill out p as much as we can (remembering it might already be populated) 596// p.addRole().setCode(convert.makeCodeableConceptFromCD(cda.getChild(assignedEntity, "code"))); 597 for (Element e : cda.getChildren(assignedEntity, "id")) 598 addToIdList(p.getIdentifier(), convert.makeIdentifierFromII(e)); 599 for (Element e : cda.getChildren(assignedEntity, "addr")) 600 if (p.getAddress() == null) 601 p.getAddress().add(convert.makeAddressFromAD(e)); 602 for (Element e : cda.getChildren(assignedEntity, "telecom")) 603 addToContactList(p.getTelecom(), convert.makeContactFromTEL(e)); 604 for (Element e : cda.getChildren(cda.getChild(assignedEntity, "assignedPerson"), "name")) 605 if (p.getName() == null) 606 p.addName(convert.makeNameFromEN(e)); 607 // todo: 608 // representedOrganization 609 return ref; 610 } 611 612 protected void addToContactList(List<ContactPoint> list, ContactPoint c) throws Exception { 613 for (ContactPoint item : list) { 614 if (Comparison.matches(item, c, null)) 615 Comparison.merge(item, c); 616 } 617 list.add(c); 618 } 619 620 protected void addToIdList(List<Identifier> list, Identifier id) throws Exception { 621 for (Identifier item : list) { 622 if (Comparison.matches(item, id, null)) 623 Comparison.merge(item, id); 624 } 625 list.add(id); 626 } 627 628 protected void addToCodeableList(List<CodeableConcept> list, CodeableConcept code) throws Exception { 629 for (CodeableConcept item : list) { 630 if (Comparison.matches(item, code, null)) 631 Comparison.merge(item, code); 632 } 633 list.add(code); 634 } 635 636 protected String getIdForEntity(Element assignedEntity) throws Exception { 637 Element id = cda.getChild(assignedEntity, "id"); // for now, just grab the first 638 if (id == null) 639 return null; 640 if (id.getAttribute("extension") == null) { 641 if (convert.isGuid(id.getAttribute("root"))) 642 return "urn:uuid:" + id.getAttribute("root"); 643 else 644 return "urn:oid:" + id.getAttribute("root"); 645 } else 646 return "ii:" + id.getAttribute("root") + "::" + id.getAttribute("extension"); 647 } 648 649 protected SectionComponent processAdverseReactionsSection(Element section) throws Exception { 650 ListResource list = new ListResource(); 651 for (Element entry : cda.getChildren(section, "entry")) { 652 Element concern = cda.getChild(entry, "act"); 653 if (cda.hasTemplateId(concern, "2.16.840.1.113883.10.20.22.4.30")) { 654 processAllergyProblemAct(list, concern); 655 } else 656 throw new Exception("Unhandled Section template ids: " + cda.showTemplateIds(concern)); 657 } 658 659 660 // todo: text 661 SectionComponent s = new Composition.SectionComponent(); 662 s.setCode(convert.makeCodeableConceptFromCD(cda.getChild(section, "code"))); 663 // todo: check subject 664 s.addEntry(Factory.makeReference(addReference(list, "Allergies, Adverse Reactions, Alerts", makeUUIDReference()))); 665 return s; 666 } 667 668 protected void processAllergyProblemAct(ListResource list, Element concern) throws Exception { 669 cda.checkTemplateId(concern, "2.16.840.1.113883.10.20.22.4.30"); 670 // Allergy Problem Act - this is a concern - we treat the concern as information about it's place in the list 671 checkNoNegationOrNullFlavor(concern, "Allergy Problem Act"); 672 checkNoSubject(concern, "Allergy Problem Act"); 673 674 // SHALL contain at least one [1..*] entryRelationship (CONF:7509) such that it 675 // SHALL contain exactly one [1..1] Allergy - intolerance Observation 676 for (Element entry : cda.getChildren(concern, "entryRelationship")) { 677 Element obs = cda.getChild(entry, "observation"); 678 cda.checkTemplateId(obs, "2.16.840.1.113883.10.20.22.4.7"); 679 checkNoNegationOrNullFlavor(obs, "Allergy - intolerance Observation"); 680 checkNoSubject(obs, "Allergy Problem Act"); 681 682 AllergyIntolerance ai = new AllergyIntolerance(); 683 ListEntryComponent item = addItemToList(list, ai); 684 685 // this first section comes from the concern, and is processed once for each observation in the concern group 686 // SHALL contain at least one [1..*] id (CONF:7472). 687 for (Element e : cda.getChildren(concern, "id")) 688 ai.getIdentifier().add(convert.makeIdentifierFromII(e)); 689 690 // SHALL contain exactly one [1..1] statusCode, which SHALL be selected from ValueSet 2.16.840.1.113883.3.88.12.80.68 HITSPProblemStatus DYNAMIC (CONF:7485) 691 // the status code is about the concern (e.g. the entry in the list) 692 // possible values: active, suspended, aborted, completed, with an effective time 693 String s = cda.getStatus(concern); 694 item.setFlag(Factory.newCodeableConcept(s, "http://hl7.org/fhir/v3/ActStatus", s)); 695 if (s.equals("aborted")) // only on this condition? 696 item.setDeleted(true); 697 698 // SHALL contain exactly one [1..1] effectiveTime (CONF:7498) 699 Period p = convert.makePeriodFromIVL(cda.getChild(concern, "effectiveTime")); 700 item.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/list-period", p, false)); 701 if (p.getEnd() != null) 702 item.setDate(p.getEnd()); 703 else 704 item.setDate(p.getStart()); 705 706 //ok, now process the actual observation 707 // SHALL contain at least one [1..*] id (CONF:7382) 708 for (Element e : cda.getChildren(obs, "id")) 709 ai.getIdentifier().add(convert.makeIdentifierFromII(e)); 710 711 712 // SHALL contain exactly one [1..1] effectiveTime (CONF:7387) 713 ai.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/allergyintolerance-period", convert.makePeriodFromIVL(cda.getChild(obs, "effectiveTime")), false)); 714 715 // SHALL contain exactly one [1..1] value with @xsi:type="CD" (CONF:7390) 716 CodeableConcept type = convert.makeCodeableConceptFromCD(cda.getChild(obs, "value")); 717 // This value SHALL contain @code, which SHALL be selected from ValueSet 2.16.840.1.113883.3.88.12.3221.6.2 Allergy/Adverse Event Type 718 String ss = type.getCoding().get(0).getCode(); 719 if (ss.equals("416098002") || ss.equals("414285001")) 720 ai.setType(AllergyIntoleranceType.ALLERGY); 721 else if (ss.equals("59037007") || ss.equals("235719002")) 722 ai.setType(AllergyIntoleranceType.INTOLERANCE); 723 ai.getExtension().add(Factory.newExtension("http://www.healthintersections.com.au/fhir/extensions/allergy-category", type, false)); 724 725 // SHOULD contain zero or one [0..1] participant (CONF:7402) such that it 726 // ......This playingEntity SHALL contain exactly one [1..1] code 727 ai.setCode(convert.makeCodeableConceptFromCD(cda.getDescendent(obs, "participant/participantRole/playingEntity/code"))); 728 729 // MAY contain zero or one [0..1] entryRelationship (CONF:7440) such that it SHALL contain exactly one [1..1] Alert Status Observation 730 // SHOULD contain zero or more [0..*] entryRelationship (CONF:7447) such that it SHALL contain exactly one [1..1] Reaction Observation (templateId:2.16.840.1.113883.10.20.22.4.9) (CONF:7450). 731 for (Element e : cda.getChildren(obs, "entryRelationship")) { 732 Element child = cda.getChild(e, "observation"); 733 if (cda.hasTemplateId(child, "2.16.840.1.113883.10.20.22.4.28") && ai.getClinicalStatus() == null) { 734 // SHALL contain exactly one [1..1] value with @xsi:type="CE", where the @code SHALL be selected from ValueSet Problem Status Value Set 2.16.840.1.113883.3.88.12.80.68 DYNAMIC (CONF:7322). 735 // 55561003 SNOMED CT Active 736 // 73425007 SNOMED CT Inactive 737 // 413322009 SNOMED CT Resolved 738 String sc = cda.getChild(child, "value").getAttribute("code"); 739 if (sc.equals("55561003")) { 740 ai.setClinicalStatus(AllergyIntoleranceClinicalStatus.ACTIVE); 741 ai.setVerificationStatus(AllergyIntoleranceVerificationStatus.CONFIRMED); 742 } else 743 ai.setClinicalStatus(AllergyIntoleranceClinicalStatus.RESOLVED); 744 } else if (cda.hasTemplateId(child, "2.16.840.1.113883.10.20.22.4.9")) { 745 ai.getReaction().add(processAdverseReactionObservation(child)); 746 } 747 } 748 749 // SHOULD contain zero or one [0..1] entryRelationship (CONF:9961) such that it SHALL contain exactly one [1..1] Severity Observation (templateId:2.16.840.1.113883.10.20.22.4.8) (CONF:9963). 750 ai.setCriticality(readCriticality(cda.getSeverity(obs))); 751 } 752 } 753 754 // this is going to be a contained resource, so we aren't going to generate any narrative 755 protected AllergyIntoleranceReactionComponent processAdverseReactionObservation(Element reaction) throws Exception { 756 checkNoNegationOrNullFlavor(reaction, "Adverse Reaction Observation"); 757 checkNoSubject(reaction, "Adverse Reaction Observation"); 758 759 // This clinical statement represents an undesired symptom, finding, etc., due to an administered or exposed substance. A reaction can be defined with respect to its severity, and can have been treated by one or more interventions. 760 AllergyIntoleranceReactionComponent ar = new AllergyIntoleranceReactionComponent(); 761 762 // SHALL contain exactly one [1..1] id (CONF:7329). 763 for (Element e : cda.getChildren(reaction, "id")) 764 ToolingExtensions.addIdentifier(ar, convert.makeIdentifierFromII(e)); 765 766 // SHALL contain exactly one [1..1] code (CONF:7327). The value set for this code element has not been specified. 767 // todo: what the heck is this? 768 769 // SHOULD contain zero or one [0..1] text (CONF:7330). 770 // todo: so what is this? how can we know whether to ignore it? 771 772 // 8. SHOULD contain zero or one [0..1] effectiveTime (CONF:7332). 773 // a. This effectiveTime SHOULD contain zero or one [0..1] low (CONF:7333). 774 // b. This effectiveTime SHOULD contain zero or one [0..1] high (CONF:7334). 775 // !this is a problem because FHIR just has a date, not a period. 776 ar.setOnsetElement(convert.makeDateTimeFromIVL(cda.getChild(reaction, "effectiveTime"))); 777 778 // SHALL contain exactly one [1..1] value with @xsi:type="CD", where the @code SHALL be selected from ValueSet 2.16.840.1.113883.3.88.12.3221.7.4 Problem DYNAMIC (CONF:7335). 779 ar.getManifestation().add(convert.makeCodeableConceptFromCD(cda.getChild(reaction, "value"))); 780 781 // SHOULD contain zero or one [0..1] entryRelationship (CONF:7580) such that it SHALL contain exactly one [1..1] Severity Observation (templateId:2.16.840.1.113883.10.20.22.4.8) (CONF:7582). 782 ar.setSeverity(readSeverity(cda.getSeverity(reaction))); 783 784 // MAY contain zero or more [0..*] entryRelationship (CONF:7337) such that it SHALL contain exactly one [1..1] Procedure Activity Procedure (templateId:2.16.840.1.113883.10.20.22.4.14) (CONF:7339). 785 // i. This procedure activity is intended to contain information about procedures that were performed in response to an allergy reaction 786 // todo: process these into an procedure and add as an extension 787 788 // MAY contain zero or more [0..*] entryRelationship (CONF:7340) such that it SHALL contain exactly one [1..1] Medication Activity (templateId:2.16.840.1.113883.10.20.22.4.16) (CONF:7342). 789 // i. This medication activity is intended to contain information about medications that were administered in response to an allergy reaction. (CONF:7584). 790 // todo: process these into an medication statement and add as an extension 791 792 return ar; 793 } 794 795 protected SectionComponent processSocialHistorySection(Element section) throws Exception { 796 ListResource list = new ListResource(); 797 for (Element entry : cda.getChildren(section, "entry")) { 798 Element observation = cda.getlastChild(entry); 799 800 if (cda.hasTemplateId(observation, "2.16.840.1.113883.10.20.22.4.38")) { 801 processSocialObservation(list, observation, SocialHistoryType.SocialHistory); 802 } else if (cda.hasTemplateId(observation, "2.16.840.1.113883.10.20.15.3.8")) { 803 processSocialObservation(list, observation, SocialHistoryType.Pregnancy); 804 } else if (cda.hasTemplateId(observation, "2.16.840.1.113883.10.20.22.4.78")) { 805 processSocialObservation(list, observation, SocialHistoryType.SmokingStatus); 806 } else if (cda.hasTemplateId(observation, "2.16.840.1.113883.10.20.22.4.85")) { 807 processSocialObservation(list, observation, SocialHistoryType.TobaccoUse); 808 } else 809 throw new Exception("Unhandled Section template ids: " + cda.showTemplateIds(observation)); 810 } 811 812 // todo: text 813 SectionComponent s = new Composition.SectionComponent(); 814 s.setCode(convert.makeCodeableConceptFromCD(cda.getChild(section, "code"))); 815 // todo: check subject 816 s.addEntry(Factory.makeReference(addReference(list, "Procedures", makeUUIDReference()))); 817 return s; 818 819 } 820 821 protected void processSocialObservation(ListResource list, Element so, SocialHistoryType type) throws Exception { 822 Observation obs = new Observation(); 823 addItemToList(list, obs); 824 825 switch (type) { 826 case SocialHistory: 827 cda.checkTemplateId(so, "2.16.840.1.113883.10.20.22.4.38"); 828 // SHALL contain exactly one [1..1] code (CONF:8558/). 829 obs.setCode(convert.makeCodeableConceptFromCD(cda.getChild(so, "code"))); 830 break; 831 case Pregnancy: 832 cda.checkTemplateId(so, "2.16.840.1.113883.10.20.15.3.8"); 833 // SHALL contain exactly one [1..1] code (CONF:8558/), which SHALL be an assertion 834 obs.setCode(Factory.newCodeableConcept("11449-6", "http://loinc.org", "Pregnancy Status")); 835 break; 836 case SmokingStatus: 837 cda.checkTemplateId(so, "2.16.840.1.113883.10.20.22.4.78"); 838 // SHALL contain exactly one [1..1] code (CONF:8558/), which SHALL be an assertion 839 obs.setCode(Factory.newCodeableConcept("72166-2", "http://loinc.org", "Tobacco Smoking Status")); 840 break; 841 case TobaccoUse: 842 cda.checkTemplateId(so, "2.16.840.1.113883.10.20.22.4.12"); 843 // SHALL contain exactly one [1..1] code (CONF:8558/), which SHALL be an assertion 844 obs.setCode(Factory.newCodeableConcept("11367-0", "http://loinc.org", "History of Tobacco Use")); 845 } 846 847 // SHALL contain at least one [1..*] id (8551). 848 for (Element e : cda.getChildren(so, "id")) 849 obs.getIdentifier().add(convert.makeIdentifierFromII(e)); 850 851 852 // SHALL contain exactly one [1..1] statusCode (CONF:8553/455/14809). 853 // a. This statusCode SHALL contain exactly one [1..1] @code="completed" Completed (CodeSystem: ActStatus 2.16.840.1.113883.5.14 STATIC) (CONF:19117). 854 obs.setStatus(ObservationStatus.FINAL); 855 856 // SHOULD contain zero or one [0..1] effectiveTime (CONF:2018/14814). 857 // for smoking status/tobacco: low only. in R2, this is just value. So we treat low only as just a value 858 Element et = cda.getChild(so, "effectiveTime"); 859 if (et != null) { 860 if (cda.getChild(et, "low") != null) 861 obs.setEffective(convert.makeDateTimeFromTS(cda.getChild(et, "low"))); 862 else 863 obs.setEffective(convert.makeDateTimeFromIVL(et)); 864 } 865 866 // SHOULD contain zero or one [0..1] value (CONF:8559). 867 // a. Observation/value can be any data type. 868 for (Element e : cda.getChildren(so, "value")) 869 if (obs.getValue() == null) { // only one in FHIR 870 // special case for pregnancy: 871 if (type == SocialHistoryType.Pregnancy && "true".equals(e.getAttribute("negationInd"))) { 872 obs.setValue(Factory.newCodeableConcept("60001007", "http://snomed.info/sct", "Not pregnant")); 873 } else { 874 // negationInd is not described. but it might well be used. For now, we blow up 875 checkNoNegation(so, "Social Observation (" + type + ")"); 876 877 if (so.hasAttribute("nullFlavor")) 878 obs.setValue(convert.makeCodeableConceptFromNullFlavor(so.getAttribute("nullFlavor"))); 879 else if (e.hasAttribute("nullFlavor") && !"OTH".equals(e.getAttribute("nullFlavor"))) 880 obs.setValue(convert.makeCodeableConceptFromNullFlavor(e.getAttribute("nullFlavor"))); 881 else 882 obs.setValue(convert.makeTypeFromANY(e)); 883 } 884 } else 885 throw new Exception("too many values on Social Observation"); 886 887 if (type == SocialHistoryType.Pregnancy) { 888 for (Element e : cda.getChildren(so, "entyRelationship")) { 889 Element dd = cda.getChild(e, "observation"); 890 checkNoNegationOrNullFlavor(dd, "Estimated Date of Delivery"); 891 // MAY contain zero or one [0..1] entryRelationship (CONF:458) such that it 892 // SHALL contain exactly one [1..1] @typeCode="REFR" Refers to (CodeSystem: HL7ActRelationshipType 2.16.840.1.113883.5.1002 STATIC) (CONF:459). 893 // SHALL contain exactly one [1..1] Estimated Date of Delivery (templateId:2.16.840.1.113883.10.20.15.3.1) (CONF:15584). 894 Observation co = new Observation(); 895 String id = nextRef(); 896 co.setId(id); 897 obs.getContained().add(co); 898 ObservationRelatedComponent or = new ObservationRelatedComponent(); 899 obs.getRelated().add(or); 900 or.setType(ObservationRelationshipType.HASMEMBER); 901 or.setTarget(Factory.makeReference("#" + id)); 902 co.setCode(Factory.newCodeableConcept("11778-8", "http://loinc.org", "Delivery date Estimated")); 903 co.setValue(convert.makeDateTimeFromTS(cda.getChild(dd, "value"))); // not legal, see gForge http://gforge.hl7.org/gf/project/fhir/tracker/?action=TrackerItemEdit&tracker_item_id=3125&start=0 904 } 905 } 906 } 907 908 protected void checkNoNegation(Element act, String path) throws Exception { 909 if ("true".equals(act.getAttribute("negationInd"))) 910 throw new Exception("The conversion program cannot accept a negationInd at the location " + path); 911 } 912 913 protected void checkNoNegationOrNullFlavor(Element act, String path) throws Exception { 914 if (act.hasAttribute("nullFlavor")) 915 throw new Exception("The conversion program cannot accept a nullFlavor at the location " + path); 916 if ("true".equals(act.getAttribute("negationInd"))) 917 throw new Exception("The conversion program cannot accept a negationInd at the location " + path); 918 } 919 920 protected ListEntryComponent addItemToList(ListResource list, DomainResource ai) 921 throws Exception { 922 list.getContained().add(ai); 923 String n = nextRef(); 924 ai.setId(n); 925 ListEntryComponent item = new ListResource.ListEntryComponent(); 926 list.getEntry().add(item); 927 item.setItem(Factory.makeReference("#" + n)); 928 return item; 929 } 930 931 protected String nextRef() { 932 refCounter++; 933 String n = refCounter.toString(); 934 return n; 935 } 936 937 protected AllergyIntoleranceCriticality readCriticality(String severity) { 938 if ("255604002".equals(severity)) // Mild 939 return AllergyIntoleranceCriticality.LOW; 940 if ("371923003".equals(severity)) // Mild to moderate 941 return AllergyIntoleranceCriticality.LOW; 942 if ("6736007".equals(severity)) // Moderate 943 return AllergyIntoleranceCriticality.LOW; 944 if ("371924009".equals(severity)) // Moderate to severe 945 return AllergyIntoleranceCriticality.HIGH; 946 if ("24484000".equals(severity)) // Severe 947 return AllergyIntoleranceCriticality.HIGH; 948 if ("399166001".equals(severity)) // Fatal 949 return AllergyIntoleranceCriticality.HIGH; 950 return null; 951 } 952 953 protected AllergyIntoleranceSeverity readSeverity(String severity) { 954 if ("255604002".equals(severity)) // Mild 955 return AllergyIntoleranceSeverity.MILD; 956 if ("371923003".equals(severity)) // Mild to moderate 957 return AllergyIntoleranceSeverity.MODERATE; 958 if ("6736007".equals(severity)) // Moderate 959 return AllergyIntoleranceSeverity.MODERATE; 960 if ("371924009".equals(severity)) // Moderate to severe 961 return AllergyIntoleranceSeverity.SEVERE; 962 if ("24484000".equals(severity)) // Severe 963 return AllergyIntoleranceSeverity.SEVERE; 964 if ("399166001".equals(severity)) // Fatal 965 return AllergyIntoleranceSeverity.SEVERE; 966 return null; 967 } 968 969 protected SectionComponent processVitalSignsSection(Element section) throws Exception { 970 ListResource list = new ListResource(); 971 for (Element entry : cda.getChildren(section, "entry")) { 972 Element organizer = cda.getlastChild(entry); 973 974 if (cda.hasTemplateId(organizer, "2.16.840.1.113883.10.20.22.4.26")) { 975 processVitalSignsOrganizer(list, organizer); 976 } else 977 throw new Exception("Unhandled Section template ids: " + cda.showTemplateIds(organizer)); 978 } 979 980 // todo: text 981 SectionComponent s = new Composition.SectionComponent(); 982 s.setCode(convert.makeCodeableConceptFromCD(cda.getChild(section, "code"))); 983 // todo: check subject 984 s.addEntry(Factory.makeReference(addReference(list, "Vital Signs", makeUUIDReference()))); 985 return s; 986 987 } 988 989 protected void processVitalSignsOrganizer(ListResource list, Element organizer) throws Exception { 990 991 cda.checkTemplateId(organizer, "2.16.840.1.113883.10.20.22.4.26"); 992 checkNoNegationOrNullFlavor(organizer, "Vital Signs Organizer"); 993 checkNoSubject(organizer, "Vital Signs Organizer"); 994 // moodCode is EVN. 995 996 Observation obs = new Observation(); 997 addItemToList(list, obs); 998 999 // SHALL contain at least one [1..*] id (CONF:7282). 1000 for (Element e : cda.getChildren(organizer, "id")) 1001 obs.getIdentifier().add(convert.makeIdentifierFromII(e)); 1002 1003 // SHALL contain exactly one [1..1] code (CONF:19176). 1004 // This code SHALL contain exactly one [1..1] @code="46680005" Vital signs (CodeSystem: SNOMED-CT 2.16.840.1.113883.6.96 STATIC) (CONF:19177). 1005 obs.setCode(convert.makeCodeableConceptFromCD(cda.getChild(organizer, "code"))); 1006 1007 1008 // SHALL contain exactly one [1..1] effectiveTime (CONF:7288). 1009 obs.setEffective(convert.makeMatchingTypeFromIVL(cda.getChild(organizer, "effectiveTime"))); 1010 1011 // SHALL contain at least one [1..*] component (CONF:7285) such that it 1012 // SHALL contain exactly one [1..1] Vital Sign Observation (templateId:2.16.840.1.113883.10.20.22.4.27) (CONF:15946). 1013 for (Element e : cda.getChildren(organizer, "component")) { 1014 ObservationRelatedComponent ro = new ObservationRelatedComponent(); 1015 ro.setType(ObservationRelationshipType.HASMEMBER); 1016 ro.setTarget(Factory.makeReference("#" + processVitalSignsObservation(e, list))); 1017 } 1018 } 1019 1020 protected String processVitalSignsObservation(Element comp, ListResource list) throws Exception { 1021 Element observation = cda.getChild(comp, "observation"); 1022 cda.checkTemplateId(observation, "2.16.840.1.113883.10.20.22.4.27"); 1023 checkNoNegationOrNullFlavor(observation, "Vital Signs Observation"); 1024 checkNoSubject(observation, "Vital Signs Observation"); 1025 1026 Observation obs = new Observation(); 1027 1028 // SHALL contain at least one [1..*] id (CONF:7300). 1029 for (Element e : cda.getChildren(observation, "id")) 1030 obs.getIdentifier().add(convert.makeIdentifierFromII(e)); 1031 1032 // SHALL contain exactly one [1..1] code, which SHOULD be selected from ValueSet Vital Sign Result Value Set 2.16.840.1.113883.3.88.12.80.62 DYNAMIC (CONF:7301). 1033 obs.setCode(convert.makeCodeableConceptFromCD(cda.getChild(observation, "code"))); // all loinc codes 1034 1035 // SHOULD contain zero or one [0..1] text (CONF:7302). 1036 // The text, if present, SHOULD contain zero or one [0..1] reference (CONF:15943). 1037 // The reference, if present, SHOULD contain zero or one [0..1] @value (CONF:15944). 1038 // This reference/@value SHALL begin with a '#' and SHALL point to its corresponding narrative (using the approach defined in CDA Release 2, section 4.3.5.1) (CONF:15945). 1039 // todo: put this in narrative if it exists? 1040 1041 1042 // SHALL contain exactly one [1..1] statusCode (CONF:7303). This statusCode SHALL contain exactly one [1..1] @code="completed" Completed (CodeSystem: ActStatus 2.16.840.1.113883.5.14 STATIC) (CONF:19119). 1043 // ignore 1044 1045 // SHALL contain exactly one [1..1] effectiveTime (CONF:7304). 1046 obs.setEffective(convert.makeMatchingTypeFromIVL(cda.getChild(observation, "effectiveTime"))); 1047 1048 // SHALL contain exactly one [1..1] value with @xsi:type="PQ" (CONF:7305). 1049 obs.setValue(convert.makeQuantityFromPQ(cda.getChild(observation, "value"))); 1050 1051 // MAY contain zero or one [0..1] interpretationCode (CONF:7307). 1052 obs.setInterpretation(convert.makeCodeableConceptFromCD(cda.getChild(observation, "interpretationCode"))); 1053 1054 // MAY contain zero or one [0..1] methodCode (CONF:7308). 1055 obs.setMethod(convert.makeCodeableConceptFromCD(cda.getChild(observation, "methodCode"))); 1056 1057 // MAY contain zero or one [0..1] targetSiteCode (CONF:7309). 1058 obs.setBodySite(convert.makeCodeableConceptFromCD(cda.getChild(observation, "targetSiteCode"))); 1059 1060 // MAY contain zero or one [0..1] author (CONF:7310). 1061 if (cda.getChild(observation, "author") != null) 1062 obs.getPerformer().add(makeReferenceToPractitionerForAssignedEntity(cda.getChild(observation, "author"), composition)); 1063 1064 // make a contained practitioner 1065 String n = nextRef(); 1066 obs.setId(n); 1067 list.getContained().add(obs); 1068 return n; 1069 } 1070 1071 public enum SocialHistoryType { 1072 SocialHistory, Pregnancy, SmokingStatus, TobaccoUse 1073 1074 } 1075 1076 1077 public enum ProcedureType { 1078 Observation, Procedure, Act 1079 1080 } 1081}