
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.term.icd10cm; 021 022import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; 023import ca.uhn.fhir.jpa.entity.TermConcept; 024import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; 025import ca.uhn.fhir.util.XmlUtil; 026import org.w3c.dom.Document; 027import org.w3c.dom.Element; 028import org.xml.sax.SAXException; 029 030import java.io.IOException; 031import java.io.Reader; 032import java.util.List; 033 034import static org.apache.commons.lang3.StringUtils.isNotBlank; 035 036public class Icd10CmLoader { 037 038 private final TermCodeSystemVersion myCodeSystemVersion; 039 private int myConceptCount; 040 private static final String SEVEN_CHR_DEF = "sevenChrDef"; 041 private static final String EXTENSION = "extension"; 042 private static final String DIAG = "diag"; 043 private static final String NAME = "name"; 044 private static final String DESC = "desc"; 045 046 /** 047 * Constructor 048 */ 049 public Icd10CmLoader(TermCodeSystemVersion theCodeSystemVersion) { 050 myCodeSystemVersion = theCodeSystemVersion; 051 } 052 053 public void load(Reader theReader) throws IOException, SAXException { 054 myConceptCount = 0; 055 056 Document document = XmlUtil.parseDocument(theReader, false, false); 057 Element documentElement = document.getDocumentElement(); 058 059 // Extract version: Should only be 1 tag 060 for (Element nextVersion : XmlUtil.getChildrenByTagName(documentElement, "version")) { 061 String versionId = nextVersion.getTextContent(); 062 if (isNotBlank(versionId)) { 063 myCodeSystemVersion.setCodeSystemVersionId(versionId); 064 } 065 } 066 067 // Extract Diags (codes) 068 for (Element nextChapter : XmlUtil.getChildrenByTagName(documentElement, "chapter")) { 069 for (Element nextSection : XmlUtil.getChildrenByTagName(nextChapter, "section")) { 070 for (Element nextDiag : XmlUtil.getChildrenByTagName(nextSection, "diag")) { 071 extractCode(nextDiag, null, null); 072 } 073 } 074 } 075 } 076 077 private void extractCode(Element theDiagElement, TermConcept theParentConcept, List<Element> theParentSevenChrDef) { 078 String code = theDiagElement.getElementsByTagName(NAME).item(0).getTextContent(); 079 String display = theDiagElement.getElementsByTagName(DESC).item(0).getTextContent(); 080 List<Element> mySevenChrDef = null; 081 TermConcept concept; 082 if (theParentConcept == null) { 083 concept = myCodeSystemVersion.addConcept(); 084 } else { 085 concept = theParentConcept.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA); 086 } 087 088 concept.setCode(code); 089 concept.setDisplay(display); 090 091 // Check for seventh character definitions. If none exist at this level, 092 // use seventh character definitions inherited from parent level. 093 if (!XmlUtil.getChildrenByTagName(theDiagElement, SEVEN_CHR_DEF).isEmpty()) { 094 mySevenChrDef = XmlUtil.getChildrenByTagName(theDiagElement, SEVEN_CHR_DEF); 095 } else if (theParentSevenChrDef != null) { 096 mySevenChrDef = theParentSevenChrDef.stream().toList(); 097 } 098 099 // If this concept has no children, apply the seventh character definitions. 100 // Otherwise create the children. 101 if (mySevenChrDef != null 102 && XmlUtil.getChildrenByTagName(theDiagElement, DIAG).isEmpty()) { 103 if (theParentConcept == null) { 104 // This is a root concept. Add the extensions as children of the current concept. 105 extractExtension(mySevenChrDef, theDiagElement, concept, true); 106 } else { 107 // This is a child concept. Add the extensions as siblings of the current concept 108 extractExtension(mySevenChrDef, theDiagElement, theParentConcept, false); 109 } 110 } else { 111 for (Element nextChildDiag : XmlUtil.getChildrenByTagName(theDiagElement, DIAG)) { 112 extractCode(nextChildDiag, concept, mySevenChrDef); 113 } 114 } 115 116 myConceptCount++; 117 } 118 119 private void extractExtension( 120 List<Element> theSevenChrDefElement, 121 Element theChildDiag, 122 TermConcept theParentConcept, 123 boolean isRootCode) { 124 for (Element nextChrNote : theSevenChrDefElement) { 125 for (Element nextExtension : XmlUtil.getChildrenByTagName(nextChrNote, EXTENSION)) { 126 String baseCode = 127 theChildDiag.getElementsByTagName(NAME).item(0).getTextContent(); 128 if (isRootCode) { 129 baseCode = baseCode + "."; 130 } 131 String sevenChar = nextExtension.getAttributes().item(0).getNodeValue(); 132 String baseDef = theChildDiag.getElementsByTagName(DESC).item(0).getTextContent(); 133 String sevenCharDef = nextExtension.getTextContent(); 134 135 TermConcept concept = theParentConcept.addChild(TermConceptParentChildLink.RelationshipTypeEnum.ISA); 136 137 concept.setCode(getExtendedCode(baseCode, sevenChar)); 138 concept.setDisplay(getExtendedDisplay(baseDef, sevenCharDef)); 139 } 140 } 141 } 142 143 private String getExtendedDisplay(String theBaseDef, String theSevenCharDef) { 144 return theBaseDef + ", " + theSevenCharDef; 145 } 146 147 /** 148 * The Seventh Character must be placed at the seventh position of the code 149 * If the base code only has five characters, "X" will be used as a placeholder 150 */ 151 private String getExtendedCode(String theBaseCode, String theSevenChar) { 152 String placeholder = "X"; 153 StringBuilder code = new StringBuilder(theBaseCode); 154 code.append(placeholder.repeat(Math.max(0, 7 - code.length()))); 155 code.append(theSevenChar); 156 return code.toString(); 157 } 158 159 public int getConceptCount() { 160 return myConceptCount; 161 } 162}