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