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}