diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java index a145187404b..35ad3d41781 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java @@ -25,7 +25,7 @@ public final class Msg { /** * IMPORTANT: Please update the following comment after you add a new code - * Last used code value: 2134 + * Last used code value: 2135 */ private Msg() {} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java index ea5fa5c08e2..fcef675e08c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java @@ -1870,10 +1870,10 @@ public class XmlUtil { } public static Document parseDocument(Reader reader) throws SAXException, IOException { - return parseDocument(reader, true); + return parseDocument(reader, true, false); } - public static Document parseDocument(Reader theReader, boolean theNamespaceAware) throws SAXException, IOException { + public static Document parseDocument(Reader theReader, boolean theNamespaceAware, boolean allowDoctypeDeclaration) throws SAXException, IOException { DocumentBuilder builder; try { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); @@ -1881,7 +1881,7 @@ public class XmlUtil { docBuilderFactory.setXIncludeAware(false); docBuilderFactory.setExpandEntityReferences(false); try { - docBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + docBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !allowDoctypeDeclaration); docBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_1_0/3734-add-support-for-international-version-ICD-10.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_1_0/3734-add-support-for-international-version-ICD-10.yaml new file mode 100644 index 00000000000..ae89d964a0e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_1_0/3734-add-support-for-international-version-ICD-10.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 3734 +title: "Added support for loading the International version of ICD-10. Thanks to kaicode for the contribution!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/tools/hapi_fhir_cli.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/tools/hapi_fhir_cli.md index 0222eab1701..ebdd03f3784 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/tools/hapi_fhir_cli.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/tools/hapi_fhir_cli.md @@ -74,19 +74,25 @@ Note that the path and exact filename of the terminology files will likely need ### SNOMED CT ``` -./hapi-fhir-cli upload-terminology -d Downloads/SnomedCT_RF2Release_INT_20160131.zip -f dstu3 -t http://localhost:8080/baseDstu3 -u http://snomed.info/sct +./hapi-fhir-cli upload-terminology -d Downloads/SnomedCT_InternationalRF2_PRODUCTION_20220131T120000Z.zip -v r4 -t http://localhost:8080/fhir -u http://snomed.info/sct ``` ### LOINC ``` -./hapi-fhir-cli upload-terminology -d Downloads/LOINC_2.54_MULTI-AXIAL_HIERARCHY.zip -d Downloads/LOINC_2.54_Text.zip -f dstu3 -t http://localhost:8080/baseDstu3 -u http://loinc.org +./hapi-fhir-cli upload-terminology -d Downloads/LOINC_2.54_MULTI-AXIAL_HIERARCHY.zip -d Downloads/LOINC_2.54_Text.zip -v r4 -t http://localhost:8080/fhir -u http://loinc.org +``` + +### ICD-10 (International Version) + +``` +./hapi-fhir-cli upload-terminology -d Downloads/icdClaML2019ens.zip -v r4 -t http://localhost:8080/fhir -u http://hl7.org/fhir/sid/icd-10 ``` ### ICD-10-CM ``` -./hapi-fhir-cli upload-terminology -d Downloads/LOINC_2.54_MULTI-AXIAL_HIERARCHY.zip -d icd10cm_tabular_2021.xml -f dstu3 -t http://localhost:8080/baseDstu3 -u http://hl7.org/fhir/sid/icd-10-cm +./hapi-fhir-cli upload-terminology -d Downloads/icd10cm_tabular_2021.xml -v r4 -t http://localhost:8080/fhir -u http://hl7.org/fhir/sid/icd-10-cm ``` # Migrate Database diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/TerminologyUploaderProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/TerminologyUploaderProvider.java index bf3f4ed57a2..670f9fd483a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/TerminologyUploaderProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/TerminologyUploaderProvider.java @@ -129,6 +129,9 @@ public class TerminologyUploaderProvider extends BaseJpaProvider { UploadStatistics stats; switch (codeSystemUrl) { + case ITermLoaderSvc.ICD10_URI: + stats = myTerminologyLoaderSvc.loadIcd10(localFiles, theRequestDetails); + break; case ITermLoaderSvc.ICD10CM_URI: stats = myTerminologyLoaderSvc.loadIcd10cm(localFiles, theRequestDetails); break; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java index 1caedace1b8..b44328c2a92 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet; +import ca.uhn.fhir.jpa.term.icd10.Icd10Loader; import ca.uhn.fhir.jpa.term.icd10cm.Icd10CmLoader; import ca.uhn.fhir.jpa.term.loinc.LoincAnswerListHandler; import ca.uhn.fhir.jpa.term.loinc.LoincAnswerListLinkHandler; @@ -300,6 +301,39 @@ public class TermLoaderSvcImpl implements ITermLoaderSvc { } } + @Override + public UploadStatistics loadIcd10(List theFiles, RequestDetails theRequestDetails) { + ourLog.info("Beginning ICD-10 processing"); + + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(ICD10_URI); + codeSystem.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); + codeSystem.setStatus(Enumerations.PublicationStatus.ACTIVE); + + TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); + int count = 0; + + try (LoadedFileDescriptors compressedDescriptors = getLoadedFileDescriptors(theFiles)) { + for (FileDescriptor nextDescriptor : compressedDescriptors.getUncompressedFileDescriptors()) { + if (nextDescriptor.getFilename().toLowerCase(Locale.US).endsWith(".xml")) { + try (InputStream inputStream = nextDescriptor.getInputStream(); + InputStreamReader reader = new InputStreamReader(inputStream, Charsets.UTF_8) ) { + Icd10Loader loader = new Icd10Loader(codeSystem, codeSystemVersion); + loader.load(reader); + count += loader.getConceptCount(); + } + } + } + } catch (IOException | SAXException e) { + throw new InternalErrorException(Msg.code(2135) + e); + } + + codeSystem.setVersion(codeSystemVersion.getCodeSystemVersionId()); + + IIdType target = storeCodeSystem(theRequestDetails, codeSystemVersion, codeSystem, null, null); + return new UploadStatistics(count, target); + } + @Override public UploadStatistics loadIcd10cm(List theFiles, RequestDetails theRequestDetails) { ourLog.info("Beginning ICD-10-cm processing"); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10/Icd10Loader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10/Icd10Loader.java new file mode 100644 index 00000000000..2a4be4e1e41 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10/Icd10Loader.java @@ -0,0 +1,121 @@ +package ca.uhn.fhir.jpa.term.icd10; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + + +import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; +import ca.uhn.fhir.jpa.entity.TermConcept; +import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; +import org.hl7.fhir.r4.model.CodeSystem; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.Reader; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static ca.uhn.fhir.util.XmlUtil.getChildrenByTagName; +import static ca.uhn.fhir.util.XmlUtil.parseDocument; + +public class Icd10Loader { + + public static final String EXPECTED_ROOT_NODE = "ClaML"; + private final CodeSystem codeSystem; + private final TermCodeSystemVersion codeSystemVersion; + private int conceptCount = 0; + + public Icd10Loader(CodeSystem codeSystem, TermCodeSystemVersion codeSystemVersion) { + this.codeSystem = codeSystem; + this.codeSystemVersion = codeSystemVersion; + } + + public void load(Reader reader) throws IOException, SAXException { + Document document = parseDocument(reader, false, true); + Element documentElement = document.getDocumentElement(); + + String rootNodeName = documentElement.getTagName(); + if (!EXPECTED_ROOT_NODE.equals(rootNodeName)) { + return; + } + + for (Element title : getChildrenByTagName(documentElement, "Title")) { + String name = title.getAttribute("name"); + if (!name.isEmpty()) { + codeSystem.setName(name); + codeSystem.setTitle(name); + } + String version = title.getAttribute("version"); + if (!version.isEmpty()) { + codeSystemVersion.setCodeSystemVersionId(version); + } + codeSystem.setDescription(title.getTextContent()); + } + + Map conceptMap = new HashMap<>(); + for (Element aClass : getChildrenByTagName(documentElement, "Class")) { + String code = aClass.getAttribute("code"); + if (code.isEmpty()) { + continue; + } + + boolean rootConcept = getChildrenByTagName(aClass, "SuperClass").isEmpty(); + TermConcept termConcept = rootConcept ? codeSystemVersion.addConcept() : new TermConcept(); + termConcept.setCode(code); + + // Preferred label and other properties + for (Element rubric : getChildrenByTagName(aClass, "Rubric")) { + String kind = rubric.getAttribute("kind"); + Optional firstLabel = getChildrenByTagName(rubric, "Label").stream().findFirst(); + if (firstLabel.isPresent()) { + String textContent = firstLabel.get().getTextContent(); + if (textContent != null && !textContent.isEmpty()) { + textContent = textContent.replace("\n", "").replace("\r", "").replace("\t", ""); + if (kind.equals("preferred")) { + termConcept.setDisplay(textContent); + } else { + termConcept.addPropertyString(kind, textContent); + } + } + } + } + + for (Element superClass : getChildrenByTagName(aClass, "SuperClass")) { + TermConcept parent = conceptMap.get(superClass.getAttribute("code")); + if (parent != null) { + parent.addChild(termConcept, TermConceptParentChildLink.RelationshipTypeEnum.ISA); + } + } + + conceptMap.put(code, termConcept); + } + + conceptCount = conceptMap.size(); + } + + public int getConceptCount() { + return conceptCount; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10cm/Icd10CmLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10cm/Icd10CmLoader.java index 02ceeecff7d..c31f0a67a3a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10cm/Icd10CmLoader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/icd10cm/Icd10CmLoader.java @@ -57,7 +57,7 @@ public class Icd10CmLoader { public void load(Reader theReader) throws IOException, SAXException { myConceptCount = 0; - Document document = XmlUtil.parseDocument(theReader, false); + Document document = XmlUtil.parseDocument(theReader, false, false); Element documentElement = document.getDocumentElement(); // Extract version: Should only be 1 tag diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/icd10/Icd10LoaderTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/icd10/Icd10LoaderTest.java new file mode 100644 index 00000000000..abe4aa54f51 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/icd10/Icd10LoaderTest.java @@ -0,0 +1,78 @@ +package ca.uhn.fhir.jpa.term.icd10; + +import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; +import ca.uhn.fhir.jpa.entity.TermConcept; +import ca.uhn.fhir.jpa.entity.TermConceptProperty; +import ca.uhn.fhir.util.ClasspathUtil; +import org.hl7.fhir.r4.model.CodeSystem; +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class Icd10LoaderTest { + + @Test + public void testLoadIcd10Cm() throws IOException, SAXException { + StringReader reader = new StringReader(ClasspathUtil.loadResource("icd/icd10-dummy-test-en.xml")); + TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); + CodeSystem codeSystem = new CodeSystem(); + Icd10Loader loader = new Icd10Loader(codeSystem, codeSystemVersion); + loader.load(reader); + + assertEquals("ICD-10-EN", codeSystem.getTitle()); + assertEquals("International Statistical Classification of Diseases and Related Health Problems 10th Revision", codeSystem.getDescription()); + assertEquals("2022-tree-expanded", codeSystemVersion.getCodeSystemVersionId()); + + List rootConcepts = new ArrayList<>(codeSystemVersion.getConcepts()); + assertEquals(2, rootConcepts.size()); + TermConcept chapterA = rootConcepts.get(0); + assertEquals("A", chapterA.getCode()); + assertEquals("Fruit", chapterA.getDisplay()); + Collection properties = chapterA.getProperties(); + assertEquals(2, properties.size()); + assertEquals("Include fruit", chapterA.getStringProperty("inclusion")); + assertEquals("Things that are not fruit", chapterA.getStringProperty("exclusion")); + + assertEquals(""" + A "Fruit" + -A1-A3 "A1 to A3 type fruit" + --A1 "Apples" + --A2 "Pears" + --A3 "Bananas" + B "Trees" + -B1-B2 "A group of trees" + --B1 "Oak trees" + --B2 "Ash trees" + """, toTree(rootConcepts)); + } + + private String toTree(List concepts) { + StringBuilder buffer = new StringBuilder(); + for (TermConcept concept : concepts) { + toTree(concept, 0, buffer); + } + return buffer.toString(); + } + + private void toTree(TermConcept concept, int indent, StringBuilder buffer) { + buffer.append("-".repeat(indent)); + buffer.append(concept.getCode()); + String display = concept.getDisplay(); + if (display != null) { + buffer.append(" \"").append(display).append("\""); + } + buffer.append("\n"); + indent++; + for (TermConcept childCode : concept.getChildCodes()) { + toTree(childCode, indent, buffer); + } + } +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/resources/icd/icd10-dummy-test-en.xml b/hapi-fhir-jpaserver-test-utilities/src/test/resources/icd/icd10-dummy-test-en.xml new file mode 100644 index 00000000000..ca4923d296f --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/resources/icd/icd10-dummy-test-en.xml @@ -0,0 +1,66 @@ + + + + International Statistical Classification of Diseases and Related Health Problems 10th Revision + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/term/api/ITermLoaderSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/term/api/ITermLoaderSvc.java index 6b3c8072656..38aa9460c17 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/term/api/ITermLoaderSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/term/api/ITermLoaderSvc.java @@ -36,6 +36,7 @@ public interface ITermLoaderSvc { String IMGTHLA_URI = "http://www.ebi.ac.uk/ipd/imgt/hla"; String LOINC_URI = "http://loinc.org"; String SCT_URI = "http://snomed.info/sct"; + String ICD10_URI = "http://hl7.org/fhir/sid/icd-10"; String ICD10CM_URI = "http://hl7.org/fhir/sid/icd-10-cm"; String IEEE_11073_10101_URI = "urn:iso:std:iso:11073:10101"; @@ -45,6 +46,10 @@ public interface ITermLoaderSvc { UploadStatistics loadSnomedCt(List theFiles, RequestDetails theRequestDetails); + default UploadStatistics loadIcd10(List theFiles, RequestDetails theRequestDetails) { + return null; + } + UploadStatistics loadIcd10cm(List theFiles, RequestDetails theRequestDetails); UploadStatistics loadCustom(String theSystem, List theFiles, RequestDetails theRequestDetails);