001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.loinc; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.jpa.entity.TermConcept; 024import ca.uhn.fhir.jpa.term.IZipContentsHandlerCsv; 025import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; 026import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 027import org.apache.commons.csv.CSVRecord; 028import org.hl7.fhir.r4.model.ConceptMap; 029import org.hl7.fhir.r4.model.Enumerations; 030import org.hl7.fhir.r4.model.ValueSet; 031 032import java.util.HashMap; 033import java.util.HashSet; 034import java.util.List; 035import java.util.Map; 036import java.util.Properties; 037import java.util.Set; 038 039import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_VERSION; 040import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CONCEPTMAP_VERSION; 041import static org.apache.commons.lang3.StringUtils.isNotBlank; 042import static org.apache.commons.lang3.StringUtils.trim; 043 044public class LoincRsnaPlaybookHandler extends BaseLoincHandler implements IZipContentsHandlerCsv { 045 046 public static final String RSNA_CODES_VS_ID = "loinc-rsna-radiology-playbook"; 047 public static final String RSNA_CODES_VS_URI = "http://loinc.org/vs/loinc-rsna-radiology-playbook"; 048 public static final String RSNA_CODES_VS_NAME = "LOINC/RSNA Radiology Playbook"; 049 public static final String RID_CS_URI = "http://www.radlex.org"; 050 /* 051 * About these being the same - Per Dan Vreeman: 052 * We had some discussion about this, and both 053 * RIDs (RadLex clinical terms) and RPIDs (Radlex Playbook Ids) 054 * belong to the same "code system" since they will never collide. 055 * The codesystem uri is "http://www.radlex.org". FYI, that's 056 * now listed on the FHIR page: 057 * https://www.hl7.org/fhir/terminologies-systems.html 058 * -ja 059 */ 060 public static final String RPID_CS_URI = RID_CS_URI; 061 private static final String CM_COPYRIGHT = 062 "The LOINC/RSNA Radiology Playbook and the LOINC Part File contain content from RadLex® (http://rsna.org/RadLex.aspx), copyright © 2005-2017, The Radiological Society of North America, Inc., available at no cost under the license at http://www.rsna.org/uploadedFiles/RSNA/Content/Informatics/RadLex_License_Agreement_and_Terms_of_Use_V2_Final.pdf."; 063 private final Map<String, TermConcept> myCode2Concept; 064 private final List<ValueSet> myValueSets; 065 private final Map<String, ValueSet> myIdToValueSet = new HashMap<>(); 066 private final Set<String> myCodesInRsnaPlaybookValueSet = new HashSet<>(); 067 068 /** 069 * Constructor 070 */ 071 public LoincRsnaPlaybookHandler( 072 Map<String, TermConcept> theCode2concept, 073 List<ValueSet> theValueSets, 074 List<ConceptMap> theConceptMaps, 075 Properties theUploadProperties, 076 String theCopyrightStatement) { 077 super(theCode2concept, theValueSets, theConceptMaps, theUploadProperties, theCopyrightStatement); 078 myCode2Concept = theCode2concept; 079 myValueSets = theValueSets; 080 } 081 082 @Override 083 public void accept(CSVRecord theRecord) { 084 085 String loincNumber = trim(theRecord.get("LoincNumber")); 086 String longCommonName = trim(theRecord.get("LongCommonName")); 087 String partNumber = trim(theRecord.get("PartNumber")); 088 String partTypeName = trim(theRecord.get("PartTypeName")); 089 String partName = trim(theRecord.get("PartName")); 090 String partSequenceOrder = trim(theRecord.get("PartSequenceOrder")); 091 String rid = trim(theRecord.get("RID")); 092 String preferredName = trim(theRecord.get("PreferredName")); 093 String rpid = trim(theRecord.get("RPID")); 094 String longName = trim(theRecord.get("LongName")); 095 096 // CodeSystem version from properties file 097 String codeSystemVersionId = myUploadProperties.getProperty(LOINC_CODESYSTEM_VERSION.getCode()); 098 099 // ConceptMap version from properties files 100 String loincRsnaCmVersion; 101 if (codeSystemVersionId != null) { 102 loincRsnaCmVersion = 103 myUploadProperties.getProperty(LOINC_CONCEPTMAP_VERSION.getCode()) + "-" + codeSystemVersionId; 104 } else { 105 loincRsnaCmVersion = myUploadProperties.getProperty(LOINC_CONCEPTMAP_VERSION.getCode()); 106 } 107 108 // RSNA Codes VS 109 ValueSet vs; 110 String rsnaCodesValueSetId; 111 if (codeSystemVersionId != null) { 112 rsnaCodesValueSetId = RSNA_CODES_VS_ID + "-" + codeSystemVersionId; 113 } else { 114 rsnaCodesValueSetId = RSNA_CODES_VS_ID; 115 } 116 if (!myIdToValueSet.containsKey(rsnaCodesValueSetId)) { 117 vs = new ValueSet(); 118 vs.setUrl(RSNA_CODES_VS_URI); 119 vs.setId(rsnaCodesValueSetId); 120 vs.setName(RSNA_CODES_VS_NAME); 121 vs.setStatus(Enumerations.PublicationStatus.ACTIVE); 122 vs.setVersion(codeSystemVersionId); 123 myIdToValueSet.put(rsnaCodesValueSetId, vs); 124 myValueSets.add(vs); 125 } else { 126 vs = myIdToValueSet.get(rsnaCodesValueSetId); 127 } 128 129 if (!myCodesInRsnaPlaybookValueSet.contains(loincNumber)) { 130 vs.getCompose() 131 .getIncludeFirstRep() 132 .setSystem(ITermLoaderSvc.LOINC_URI) 133 .setVersion(codeSystemVersionId) 134 .addConcept() 135 .setCode(loincNumber) 136 .setDisplay(longCommonName); 137 myCodesInRsnaPlaybookValueSet.add(loincNumber); 138 } 139 140 String loincCodePropName; 141 switch (partTypeName.toLowerCase()) { 142 case "rad.anatomic location.region imaged": 143 loincCodePropName = "rad-anatomic-location-region-imaged"; 144 break; 145 case "rad.anatomic location.imaging focus": 146 loincCodePropName = "rad-anatomic-location-imaging-focus"; 147 break; 148 case "rad.modality.modality type": 149 loincCodePropName = "rad-modality-modality-type"; 150 break; 151 case "rad.modality.modality subtype": 152 loincCodePropName = "rad-modality-modality-subtype"; 153 break; 154 case "rad.anatomic location.laterality": 155 loincCodePropName = "rad-anatomic-location-laterality"; 156 break; 157 case "rad.anatomic location.laterality.presence": 158 loincCodePropName = "rad-anatomic-location-laterality-presence"; 159 break; 160 case "rad.guidance for.action": 161 loincCodePropName = "rad-guidance-for-action"; 162 break; 163 case "rad.guidance for.approach": 164 loincCodePropName = "rad-guidance-for-approach"; 165 break; 166 case "rad.guidance for.object": 167 loincCodePropName = "rad-guidance-for-object"; 168 break; 169 case "rad.guidance for.presence": 170 loincCodePropName = "rad-guidance-for-presence"; 171 break; 172 case "rad.maneuver.maneuver type": 173 loincCodePropName = "rad-maneuver-maneuver-type"; 174 break; 175 case "rad.pharmaceutical.route": 176 loincCodePropName = "rad-pharmaceutical-route"; 177 break; 178 case "rad.pharmaceutical.substance given": 179 loincCodePropName = "rad-pharmaceutical-substance-given"; 180 break; 181 case "rad.reason for exam": 182 loincCodePropName = "rad-reason-for-exam"; 183 break; 184 case "rad.subject": 185 loincCodePropName = "rad-subject"; 186 break; 187 case "rad.timing": 188 loincCodePropName = "rad-timing"; 189 break; 190 case "rad.view.aggregation": 191 loincCodePropName = "rad-view-view-aggregation"; 192 break; 193 case "rad.view.view type": 194 loincCodePropName = "rad-view-view-type"; 195 break; 196 default: 197 throw new InternalErrorException(Msg.code(912) + "Unknown PartTypeName: " + partTypeName); 198 } 199 200 TermConcept code = myCode2Concept.get(loincNumber); 201 if (code != null) { 202 code.addPropertyCoding(loincCodePropName, ITermLoaderSvc.LOINC_URI, partNumber, partName); 203 } 204 205 String partConceptMapId; 206 String termConceptMapId; 207 if (codeSystemVersionId != null) { 208 partConceptMapId = 209 LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_ID + "-" + codeSystemVersionId; 210 termConceptMapId = 211 LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_ID + "-" + codeSystemVersionId; 212 } else { 213 partConceptMapId = LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_ID; 214 termConceptMapId = LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_ID; 215 } 216 217 // LOINC Part -> Radlex RID code mappings 218 if (isNotBlank(rid)) { 219 addConceptMapEntry( 220 new ConceptMapping() 221 .setConceptMapId(partConceptMapId) 222 .setConceptMapUri(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_URI) 223 .setConceptMapVersion(loincRsnaCmVersion) 224 .setConceptMapName(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_NAME) 225 .setSourceCodeSystem(ITermLoaderSvc.LOINC_URI) 226 .setSourceCodeSystemVersion(codeSystemVersionId) 227 .setSourceCode(partNumber) 228 .setSourceDisplay(partName) 229 .setTargetCodeSystem(RID_CS_URI) 230 .setTargetCode(rid) 231 .setTargetDisplay(preferredName) 232 .setEquivalence(Enumerations.ConceptMapEquivalence.EQUAL), 233 myLoincCopyrightStatement + " " + CM_COPYRIGHT); 234 } 235 236 // LOINC Term -> Radlex RPID code mappings 237 if (isNotBlank(rpid)) { 238 addConceptMapEntry( 239 new ConceptMapping() 240 .setConceptMapId(termConceptMapId) 241 .setConceptMapUri(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_URI) 242 .setConceptMapVersion(loincRsnaCmVersion) 243 .setConceptMapName(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_NAME) 244 .setSourceCodeSystem(ITermLoaderSvc.LOINC_URI) 245 .setSourceCodeSystemVersion(codeSystemVersionId) 246 .setSourceCode(loincNumber) 247 .setSourceDisplay(longCommonName) 248 .setTargetCodeSystem(RPID_CS_URI) 249 .setTargetCode(rpid) 250 .setTargetDisplay(longName) 251 .setEquivalence(Enumerations.ConceptMapEquivalence.EQUAL), 252 myLoincCopyrightStatement + " " + CM_COPYRIGHT); 253 } 254 } 255}