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