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