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; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; 025import ca.uhn.fhir.jpa.entity.TermConcept; 026import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; 027import ca.uhn.fhir.jpa.entity.TermConceptProperty; 028import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; 029import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; 030import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; 031import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet; 032import ca.uhn.fhir.jpa.term.icd10.Icd10Loader; 033import ca.uhn.fhir.jpa.term.icd10cm.Icd10CmLoader; 034import ca.uhn.fhir.jpa.term.loinc.LoincAnswerListHandler; 035import ca.uhn.fhir.jpa.term.loinc.LoincAnswerListLinkHandler; 036import ca.uhn.fhir.jpa.term.loinc.LoincCodingPropertiesHandler; 037import ca.uhn.fhir.jpa.term.loinc.LoincConsumerNameHandler; 038import ca.uhn.fhir.jpa.term.loinc.LoincDocumentOntologyHandler; 039import ca.uhn.fhir.jpa.term.loinc.LoincGroupFileHandler; 040import ca.uhn.fhir.jpa.term.loinc.LoincGroupTermsFileHandler; 041import ca.uhn.fhir.jpa.term.loinc.LoincHandler; 042import ca.uhn.fhir.jpa.term.loinc.LoincHierarchyHandler; 043import ca.uhn.fhir.jpa.term.loinc.LoincIeeeMedicalDeviceCodeHandler; 044import ca.uhn.fhir.jpa.term.loinc.LoincImagingDocumentCodeHandler; 045import ca.uhn.fhir.jpa.term.loinc.LoincLinguisticVariantHandler; 046import ca.uhn.fhir.jpa.term.loinc.LoincLinguisticVariantsHandler; 047import ca.uhn.fhir.jpa.term.loinc.LoincMapToHandler; 048import ca.uhn.fhir.jpa.term.loinc.LoincParentGroupFileHandler; 049import ca.uhn.fhir.jpa.term.loinc.LoincPartHandler; 050import ca.uhn.fhir.jpa.term.loinc.LoincPartLinkHandler; 051import ca.uhn.fhir.jpa.term.loinc.LoincPartRelatedCodeMappingHandler; 052import ca.uhn.fhir.jpa.term.loinc.LoincRsnaPlaybookHandler; 053import ca.uhn.fhir.jpa.term.loinc.LoincTop2000LabResultsSiHandler; 054import ca.uhn.fhir.jpa.term.loinc.LoincTop2000LabResultsUsHandler; 055import ca.uhn.fhir.jpa.term.loinc.LoincUniversalOrderSetHandler; 056import ca.uhn.fhir.jpa.term.loinc.LoincXmlFileZipContentsHandler; 057import ca.uhn.fhir.jpa.term.loinc.PartTypeAndPartName; 058import ca.uhn.fhir.jpa.term.snomedct.SctHandlerConcept; 059import ca.uhn.fhir.jpa.term.snomedct.SctHandlerDescription; 060import ca.uhn.fhir.jpa.term.snomedct.SctHandlerRelationship; 061import ca.uhn.fhir.jpa.util.Counter; 062import ca.uhn.fhir.rest.api.EncodingEnum; 063import ca.uhn.fhir.rest.api.server.RequestDetails; 064import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 065import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 066import ca.uhn.fhir.util.ValidateUtil; 067import com.google.common.annotations.VisibleForTesting; 068import com.google.common.base.Charsets; 069import jakarta.annotation.Nonnull; 070import org.apache.commons.csv.CSVFormat; 071import org.apache.commons.csv.CSVParser; 072import org.apache.commons.csv.CSVRecord; 073import org.apache.commons.csv.QuoteMode; 074import org.apache.commons.io.IOUtils; 075import org.apache.commons.lang3.ObjectUtils; 076import org.apache.commons.lang3.StringUtils; 077import org.apache.commons.lang3.Validate; 078import org.hl7.fhir.instance.model.api.IIdType; 079import org.hl7.fhir.r4.model.CodeSystem; 080import org.hl7.fhir.r4.model.ConceptMap; 081import org.hl7.fhir.r4.model.Enumerations; 082import org.hl7.fhir.r4.model.ValueSet; 083import org.springframework.aop.support.AopUtils; 084import org.springframework.beans.factory.annotation.Autowired; 085import org.xml.sax.SAXException; 086 087import java.io.IOException; 088import java.io.InputStream; 089import java.io.InputStreamReader; 090import java.io.LineNumberReader; 091import java.io.Reader; 092import java.util.ArrayList; 093import java.util.Arrays; 094import java.util.Collections; 095import java.util.Date; 096import java.util.HashMap; 097import java.util.HashSet; 098import java.util.Iterator; 099import java.util.List; 100import java.util.Locale; 101import java.util.Map; 102import java.util.Map.Entry; 103import java.util.Optional; 104import java.util.Properties; 105import java.util.Set; 106import java.util.stream.Collectors; 107 108import static ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc.MAKE_LOADING_VERSION_CURRENT; 109import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_FILE; 110import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_FILE_DEFAULT; 111import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_FILE; 112import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_FILE_DEFAULT; 113import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_MAKE_CURRENT; 114import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_VERSION; 115import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CONSUMER_NAME_FILE; 116import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CONSUMER_NAME_FILE_DEFAULT; 117import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_DOCUMENT_ONTOLOGY_FILE; 118import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT; 119import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_FILE; 120import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_FILE_DEFAULT; 121import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_FILE; 122import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_FILE_DEFAULT; 123import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_TERMS_FILE; 124import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_TERMS_FILE_DEFAULT; 125import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_HIERARCHY_FILE; 126import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_HIERARCHY_FILE_DEFAULT; 127import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE; 128import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT; 129import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IMAGING_DOCUMENT_CODES_FILE; 130import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT; 131import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_LINGUISTIC_VARIANTS_FILE; 132import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_LINGUISTIC_VARIANTS_FILE_DEFAULT; 133import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_LINGUISTIC_VARIANTS_PATH; 134import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_LINGUISTIC_VARIANTS_PATH_DEFAULT; 135import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_MAPTO_FILE; 136import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_MAPTO_FILE_DEFAULT; 137import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PARENT_GROUP_FILE; 138import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PARENT_GROUP_FILE_DEFAULT; 139import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_FILE; 140import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_FILE_DEFAULT; 141import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE; 142import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_DEFAULT; 143import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_PRIMARY; 144import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_PRIMARY_DEFAULT; 145import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_SUPPLEMENTARY; 146import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT; 147import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_RELATED_CODE_MAPPING_FILE; 148import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT; 149import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_RSNA_PLAYBOOK_FILE; 150import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_RSNA_PLAYBOOK_FILE_DEFAULT; 151import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE; 152import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT; 153import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE; 154import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT; 155import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE; 156import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT; 157import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UPLOAD_PROPERTIES_FILE; 158import static org.apache.commons.lang3.StringUtils.isBlank; 159import static org.apache.commons.lang3.StringUtils.isNotBlank; 160import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_ALL_VALUESET_ID; 161import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_GENERIC_VALUESET_URL; 162 163public class TermLoaderSvcImpl implements ITermLoaderSvc { 164 public static final String CUSTOM_CONCEPTS_FILE = "concepts.csv"; 165 public static final String CUSTOM_HIERARCHY_FILE = "hierarchy.csv"; 166 public static final String CUSTOM_PROPERTIES_FILE = "properties.csv"; 167 static final String IMGTHLA_HLA_NOM_TXT = "hla_nom.txt"; 168 static final String IMGTHLA_HLA_XML = "hla.xml"; 169 static final String CUSTOM_CODESYSTEM_JSON = "codesystem.json"; 170 private static final String SCT_FILE_CONCEPT = "Terminology/sct2_Concept_Full_"; 171 private static final String SCT_FILE_DESCRIPTION = "Terminology/sct2_Description_Full"; 172 private static final String SCT_FILE_RELATIONSHIP = "Terminology/sct2_Relationship_Full"; 173 private static final String CUSTOM_CODESYSTEM_XML = "codesystem.xml"; 174 175 private static final int LOG_INCREMENT = 1000; 176 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TermLoaderSvcImpl.class); 177 // FYI: Hardcoded to R4 because that's what the term svc uses internally 178 private final FhirContext myCtx = FhirContext.forR4Cached(); 179 private final ITermDeferredStorageSvc myDeferredStorageSvc; 180 private final ITermCodeSystemStorageSvc myCodeSystemStorageSvc; 181 182 @Autowired 183 public TermLoaderSvcImpl( 184 ITermDeferredStorageSvc theDeferredStorageSvc, ITermCodeSystemStorageSvc theCodeSystemStorageSvc) { 185 this(theDeferredStorageSvc, theCodeSystemStorageSvc, true); 186 } 187 188 private TermLoaderSvcImpl( 189 ITermDeferredStorageSvc theDeferredStorageSvc, 190 ITermCodeSystemStorageSvc theCodeSystemStorageSvc, 191 boolean theProxyCheck) { 192 if (theProxyCheck) { 193 // If these validations start failing, it likely means a cyclic dependency has been introduced into the 194 // Spring Application 195 // Context that is preventing the Spring auto-proxy bean post-processor from being able to proxy these 196 // beans. Check 197 // for recent changes to the Spring @Configuration that may have caused this. 198 Validate.isTrue( 199 AopUtils.isAopProxy(theDeferredStorageSvc), 200 theDeferredStorageSvc.getClass().getName() 201 + " is not a proxy. @Transactional annotations will be ignored."); 202 Validate.isTrue( 203 AopUtils.isAopProxy(theCodeSystemStorageSvc), 204 theCodeSystemStorageSvc.getClass().getName() 205 + " is not a proxy. @Transactional annotations will be ignored."); 206 } 207 myDeferredStorageSvc = theDeferredStorageSvc; 208 myCodeSystemStorageSvc = theCodeSystemStorageSvc; 209 } 210 211 @VisibleForTesting 212 public static TermLoaderSvcImpl withoutProxyCheck( 213 ITermDeferredStorageSvc theTermDeferredStorageSvc, ITermCodeSystemStorageSvc theTermCodeSystemStorageSvc) { 214 return new TermLoaderSvcImpl(theTermDeferredStorageSvc, theTermCodeSystemStorageSvc, false); 215 } 216 217 @Override 218 public UploadStatistics loadImgthla(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 219 try (LoadedFileDescriptors descriptors = getLoadedFileDescriptors(theFiles)) { 220 List<String> mandatoryFilenameFragments = Arrays.asList(IMGTHLA_HLA_NOM_TXT, IMGTHLA_HLA_XML); 221 descriptors.verifyMandatoryFilesExist(mandatoryFilenameFragments); 222 223 ourLog.info("Beginning IMGTHLA processing"); 224 225 return processImgthlaFiles(descriptors, theRequestDetails); 226 } 227 } 228 229 @VisibleForTesting 230 LoadedFileDescriptors getLoadedFileDescriptors(List<FileDescriptor> theFiles) { 231 return new LoadedFileDescriptors(theFiles); 232 } 233 234 @Override 235 public UploadStatistics loadLoinc(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 236 try (LoadedFileDescriptors descriptors = getLoadedFileDescriptors(theFiles)) { 237 Properties uploadProperties = getProperties(descriptors, LOINC_UPLOAD_PROPERTIES_FILE.getCode()); 238 239 String codeSystemVersionId = uploadProperties.getProperty(LOINC_CODESYSTEM_VERSION.getCode()); 240 boolean isMakeCurrentVersion = 241 Boolean.parseBoolean(uploadProperties.getProperty(LOINC_CODESYSTEM_MAKE_CURRENT.getCode(), "true")); 242 243 if (StringUtils.isBlank(codeSystemVersionId) && !isMakeCurrentVersion) { 244 throw new InvalidRequestException( 245 Msg.code(864) + "'" + LOINC_CODESYSTEM_VERSION.getCode() + "' property is required when '" 246 + LOINC_CODESYSTEM_MAKE_CURRENT.getCode() + "' property is 'false'"); 247 } 248 249 List<String> mandatoryFilenameFragments = Arrays.asList( 250 uploadProperties.getProperty( 251 LOINC_ANSWERLIST_FILE.getCode(), LOINC_ANSWERLIST_FILE_DEFAULT.getCode()), 252 uploadProperties.getProperty( 253 LOINC_ANSWERLIST_LINK_FILE.getCode(), LOINC_ANSWERLIST_LINK_FILE_DEFAULT.getCode()), 254 uploadProperties.getProperty( 255 LOINC_DOCUMENT_ONTOLOGY_FILE.getCode(), LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT.getCode()), 256 uploadProperties.getProperty(LOINC_FILE.getCode(), LOINC_FILE_DEFAULT.getCode()), 257 uploadProperties.getProperty( 258 LOINC_HIERARCHY_FILE.getCode(), LOINC_HIERARCHY_FILE_DEFAULT.getCode()), 259 uploadProperties.getProperty( 260 LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE.getCode(), 261 LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT.getCode()), 262 uploadProperties.getProperty( 263 LOINC_IMAGING_DOCUMENT_CODES_FILE.getCode(), 264 LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT.getCode()), 265 uploadProperties.getProperty(LOINC_PART_FILE.getCode(), LOINC_PART_FILE_DEFAULT.getCode()), 266 uploadProperties.getProperty( 267 LOINC_PART_RELATED_CODE_MAPPING_FILE.getCode(), 268 LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT.getCode()), 269 uploadProperties.getProperty( 270 LOINC_RSNA_PLAYBOOK_FILE.getCode(), LOINC_RSNA_PLAYBOOK_FILE_DEFAULT.getCode()), 271 uploadProperties.getProperty( 272 LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE.getCode(), 273 LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT.getCode())); 274 descriptors.verifyMandatoryFilesExist(mandatoryFilenameFragments); 275 276 List<String> splitPartLinkFilenameFragments = Arrays.asList( 277 uploadProperties.getProperty( 278 LOINC_PART_LINK_FILE_PRIMARY.getCode(), LOINC_PART_LINK_FILE_PRIMARY_DEFAULT.getCode()), 279 uploadProperties.getProperty( 280 LOINC_PART_LINK_FILE_SUPPLEMENTARY.getCode(), 281 LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT.getCode())); 282 descriptors.verifyPartLinkFilesExist( 283 splitPartLinkFilenameFragments, 284 uploadProperties.getProperty( 285 LOINC_PART_LINK_FILE.getCode(), LOINC_PART_LINK_FILE_DEFAULT.getCode())); 286 287 List<String> optionalFilenameFragments = Arrays.asList( 288 uploadProperties.getProperty(LOINC_GROUP_FILE.getCode(), LOINC_GROUP_FILE_DEFAULT.getCode()), 289 uploadProperties.getProperty( 290 LOINC_GROUP_TERMS_FILE.getCode(), LOINC_GROUP_TERMS_FILE_DEFAULT.getCode()), 291 uploadProperties.getProperty( 292 LOINC_PARENT_GROUP_FILE.getCode(), LOINC_PARENT_GROUP_FILE_DEFAULT.getCode()), 293 uploadProperties.getProperty( 294 LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE.getCode(), 295 LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT.getCode()), 296 uploadProperties.getProperty( 297 LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE.getCode(), 298 LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT.getCode()), 299 uploadProperties.getProperty(LOINC_MAPTO_FILE.getCode(), LOINC_MAPTO_FILE_DEFAULT.getCode()), 300 301 // -- optional consumer name 302 uploadProperties.getProperty( 303 LOINC_CONSUMER_NAME_FILE.getCode(), LOINC_CONSUMER_NAME_FILE_DEFAULT.getCode()), 304 uploadProperties.getProperty( 305 LOINC_LINGUISTIC_VARIANTS_FILE.getCode(), 306 LOINC_LINGUISTIC_VARIANTS_FILE_DEFAULT.getCode())); 307 308 descriptors.verifyOptionalFilesExist(optionalFilenameFragments); 309 310 ourLog.info("Beginning LOINC processing"); 311 312 if (isMakeCurrentVersion) { 313 if (codeSystemVersionId != null) { 314 processLoincFiles(descriptors, theRequestDetails, uploadProperties, false); 315 uploadProperties.remove(LOINC_CODESYSTEM_VERSION.getCode()); 316 } 317 ourLog.info("Uploading CodeSystem and making it current version"); 318 319 } else { 320 ourLog.info("Uploading CodeSystem without updating current version"); 321 } 322 323 theRequestDetails.getUserData().put(MAKE_LOADING_VERSION_CURRENT, isMakeCurrentVersion); 324 return processLoincFiles(descriptors, theRequestDetails, uploadProperties, true); 325 } 326 } 327 328 @Override 329 public UploadStatistics loadSnomedCt(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 330 try (LoadedFileDescriptors descriptors = getLoadedFileDescriptors(theFiles)) { 331 332 List<String> expectedFilenameFragments = 333 Arrays.asList(SCT_FILE_DESCRIPTION, SCT_FILE_RELATIONSHIP, SCT_FILE_CONCEPT); 334 descriptors.verifyMandatoryFilesExist(expectedFilenameFragments); 335 336 ourLog.info("Beginning SNOMED CT processing"); 337 338 return processSnomedCtFiles(descriptors, theRequestDetails); 339 } 340 } 341 342 @Override 343 public UploadStatistics loadIcd10(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 344 ourLog.info("Beginning ICD-10 processing"); 345 346 CodeSystem codeSystem = new CodeSystem(); 347 codeSystem.setUrl(ICD10_URI); 348 codeSystem.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); 349 codeSystem.setStatus(Enumerations.PublicationStatus.ACTIVE); 350 351 TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); 352 int count = 0; 353 354 try (LoadedFileDescriptors compressedDescriptors = getLoadedFileDescriptors(theFiles)) { 355 for (FileDescriptor nextDescriptor : compressedDescriptors.getUncompressedFileDescriptors()) { 356 if (nextDescriptor.getFilename().toLowerCase(Locale.US).endsWith(".xml")) { 357 try (InputStream inputStream = nextDescriptor.getInputStream(); 358 InputStreamReader reader = new InputStreamReader(inputStream, Charsets.UTF_8)) { 359 Icd10Loader loader = new Icd10Loader(codeSystem, codeSystemVersion); 360 loader.load(reader); 361 count += loader.getConceptCount(); 362 } 363 } 364 } 365 } catch (IOException | SAXException e) { 366 throw new InternalErrorException(Msg.code(2135) + e); 367 } 368 369 codeSystem.setVersion(codeSystemVersion.getCodeSystemVersionId()); 370 371 IIdType target = storeCodeSystem(theRequestDetails, codeSystemVersion, codeSystem, null, null); 372 return new UploadStatistics(count, target); 373 } 374 375 @Override 376 public UploadStatistics loadIcd10cm(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 377 ourLog.info("Beginning ICD-10-cm processing"); 378 379 CodeSystem cs = new CodeSystem(); 380 cs.setUrl(ICD10CM_URI); 381 cs.setName("ICD-10-CM"); 382 cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); 383 cs.setStatus(Enumerations.PublicationStatus.ACTIVE); 384 385 TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); 386 int count = 0; 387 388 try (LoadedFileDescriptors compressedDescriptors = getLoadedFileDescriptors(theFiles)) { 389 for (FileDescriptor nextDescriptor : compressedDescriptors.getUncompressedFileDescriptors()) { 390 if (nextDescriptor.getFilename().toLowerCase(Locale.US).endsWith(".xml")) { 391 try (InputStream inputStream = nextDescriptor.getInputStream(); 392 InputStreamReader reader = new InputStreamReader(inputStream, Charsets.UTF_8)) { 393 Icd10CmLoader loader = new Icd10CmLoader(codeSystemVersion); 394 loader.load(reader); 395 count += loader.getConceptCount(); 396 } 397 } 398 } 399 } catch (IOException | SAXException e) { 400 throw new InternalErrorException(Msg.code(865) + e); 401 } 402 403 cs.setVersion(codeSystemVersion.getCodeSystemVersionId()); 404 405 IIdType target = storeCodeSystem(theRequestDetails, codeSystemVersion, cs, null, null); 406 return new UploadStatistics(count, target); 407 } 408 409 @Override 410 public UploadStatistics loadCustom( 411 String theSystem, List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 412 try (LoadedFileDescriptors descriptors = getLoadedFileDescriptors(theFiles)) { 413 Optional<String> codeSystemContent = loadFile(descriptors, CUSTOM_CODESYSTEM_JSON, CUSTOM_CODESYSTEM_XML); 414 CodeSystem codeSystem; 415 if (codeSystemContent.isPresent()) { 416 codeSystem = EncodingEnum.detectEncoding(codeSystemContent.get()) 417 .newParser(myCtx) 418 .parseResource(CodeSystem.class, codeSystemContent.get()); 419 ValidateUtil.isTrueOrThrowInvalidRequest( 420 theSystem.equalsIgnoreCase(codeSystem.getUrl()), 421 "CodeSystem.url does not match the supplied system: %s", 422 theSystem); 423 ValidateUtil.isTrueOrThrowInvalidRequest( 424 CodeSystem.CodeSystemContentMode.NOTPRESENT.equals(codeSystem.getContent()), 425 "CodeSystem.content does not match the expected value: %s", 426 CodeSystem.CodeSystemContentMode.NOTPRESENT.toCode()); 427 } else { 428 codeSystem = new CodeSystem(); 429 codeSystem.setUrl(theSystem); 430 codeSystem.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); 431 } 432 433 CustomTerminologySet terminologySet = CustomTerminologySet.load(descriptors, false); 434 TermCodeSystemVersion csv = terminologySet.toCodeSystemVersion(); 435 436 IIdType target = storeCodeSystem(theRequestDetails, csv, codeSystem, null, null); 437 return new UploadStatistics(terminologySet.getSize(), target); 438 } 439 } 440 441 @Override 442 public UploadStatistics loadDeltaAdd( 443 String theSystem, List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 444 ourLog.info( 445 "Processing terminology delta ADD for system[{}] with files: {}", 446 theSystem, 447 theFiles.stream().map(FileDescriptor::getFilename).collect(Collectors.toList())); 448 try (LoadedFileDescriptors descriptors = getLoadedFileDescriptors(theFiles)) { 449 CustomTerminologySet terminologySet = CustomTerminologySet.load(descriptors, false); 450 return myCodeSystemStorageSvc.applyDeltaCodeSystemsAdd(theSystem, terminologySet); 451 } 452 } 453 454 @Override 455 public UploadStatistics loadDeltaRemove( 456 String theSystem, List<FileDescriptor> theFiles, RequestDetails theRequestDetails) { 457 ourLog.info( 458 "Processing terminology delta REMOVE for system[{}] with files: {}", 459 theSystem, 460 theFiles.stream().map(FileDescriptor::getFilename).collect(Collectors.toList())); 461 try (LoadedFileDescriptors descriptors = getLoadedFileDescriptors(theFiles)) { 462 CustomTerminologySet terminologySet = CustomTerminologySet.load(descriptors, true); 463 return myCodeSystemStorageSvc.applyDeltaCodeSystemsRemove(theSystem, terminologySet); 464 } 465 } 466 467 private void dropCircularRefs( 468 TermConcept theConcept, ArrayList<String> theChain, Map<String, TermConcept> theCode2concept) { 469 470 theChain.add(theConcept.getCode()); 471 for (Iterator<TermConceptParentChildLink> childIter = 472 theConcept.getChildren().iterator(); 473 childIter.hasNext(); ) { 474 TermConceptParentChildLink next = childIter.next(); 475 TermConcept nextChild = next.getChild(); 476 if (theChain.contains(nextChild.getCode())) { 477 478 StringBuilder b = new StringBuilder(); 479 b.append("Removing circular reference code "); 480 b.append(nextChild.getCode()); 481 b.append(" from parent "); 482 b.append(next.getParent().getCode()); 483 b.append(". Chain was: "); 484 for (String nextInChain : theChain) { 485 TermConcept nextCode = theCode2concept.get(nextInChain); 486 b.append(nextCode.getCode()); 487 b.append('['); 488 b.append(StringUtils.substring(nextCode.getDisplay(), 0, 20) 489 .replace("[", "") 490 .replace("]", "") 491 .trim()); 492 b.append("] "); 493 } 494 ourLog.info(b.toString(), theConcept.getCode()); 495 childIter.remove(); 496 nextChild.getParents().remove(next); 497 498 } else { 499 dropCircularRefs(nextChild, theChain, theCode2concept); 500 } 501 } 502 theChain.remove(theChain.size() - 1); 503 } 504 505 @VisibleForTesting 506 @Nonnull 507 Properties getProperties(LoadedFileDescriptors theDescriptors, String thePropertiesFile) { 508 Properties retVal = new Properties(); 509 510 try (InputStream propertyStream = ca.uhn.fhir.jpa.term.TermLoaderSvcImpl.class.getResourceAsStream( 511 "/ca/uhn/fhir/jpa/term/loinc/loincupload.properties")) { 512 retVal.load(propertyStream); 513 } catch (IOException e) { 514 throw new InternalErrorException(Msg.code(866) + "Failed to process loinc.properties", e); 515 } 516 517 for (FileDescriptor next : theDescriptors.getUncompressedFileDescriptors()) { 518 if (next.getFilename().endsWith(thePropertiesFile)) { 519 try { 520 try (InputStream inputStream = next.getInputStream()) { 521 retVal.load(inputStream); 522 } 523 } catch (IOException e) { 524 throw new InternalErrorException(Msg.code(867) + "Failed to read " + thePropertiesFile, e); 525 } 526 } 527 } 528 return retVal; 529 } 530 531 private Optional<String> loadFile(LoadedFileDescriptors theDescriptors, String... theFilenames) { 532 for (FileDescriptor next : theDescriptors.getUncompressedFileDescriptors()) { 533 for (String nextFilename : theFilenames) { 534 if (next.getFilename().endsWith(nextFilename)) { 535 try { 536 String contents = IOUtils.toString(next.getInputStream(), Charsets.UTF_8); 537 return Optional.of(contents); 538 } catch (IOException e) { 539 throw new InternalErrorException(Msg.code(868) + e); 540 } 541 } 542 } 543 } 544 return Optional.empty(); 545 } 546 547 private UploadStatistics processImgthlaFiles( 548 LoadedFileDescriptors theDescriptors, RequestDetails theRequestDetails) { 549 final TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); 550 final List<ValueSet> valueSets = new ArrayList<>(); 551 final List<ConceptMap> conceptMaps = new ArrayList<>(); 552 553 CodeSystem imgthlaCs; 554 try { 555 String imgthlaCsString = IOUtils.toString( 556 TermReadSvcImpl.class.getResourceAsStream("/ca/uhn/fhir/jpa/term/imgthla/imgthla.xml"), 557 Charsets.UTF_8); 558 imgthlaCs = FhirContext.forR4Cached().newXmlParser().parseResource(CodeSystem.class, imgthlaCsString); 559 } catch (IOException e) { 560 throw new InternalErrorException(Msg.code(869) + "Failed to load imgthla.xml", e); 561 } 562 563 boolean foundHlaNom = false; 564 boolean foundHlaXml = false; 565 for (FileDescriptor nextZipBytes : theDescriptors.getUncompressedFileDescriptors()) { 566 String nextFilename = nextZipBytes.getFilename(); 567 568 if (!IMGTHLA_HLA_NOM_TXT.equals(nextFilename) 569 && !nextFilename.endsWith("/" + IMGTHLA_HLA_NOM_TXT) 570 && !IMGTHLA_HLA_XML.equals(nextFilename) 571 && !nextFilename.endsWith("/" + IMGTHLA_HLA_XML)) { 572 ourLog.info("Skipping unexpected file {}", nextFilename); 573 continue; 574 } 575 576 if (IMGTHLA_HLA_NOM_TXT.equals(nextFilename) || nextFilename.endsWith("/" + IMGTHLA_HLA_NOM_TXT)) { 577 // process colon-delimited hla_nom.txt file 578 ourLog.info("Processing file {}", nextFilename); 579 580 // IRecordHandler handler = new HlaNomTxtHandler(codeSystemVersion, code2concept, 581 // propertyNamesToTypes); 582 // AntigenSource antigenSource = new WmdaAntigenSource(hlaNomFilename, relSerSerFilename, 583 // relDnaSerFilename); 584 585 Reader reader = null; 586 try { 587 reader = new InputStreamReader(nextZipBytes.getInputStream(), Charsets.UTF_8); 588 589 LineNumberReader lnr = new LineNumberReader(reader); 590 while (lnr.readLine() != null) {} 591 ourLog.warn("Lines read from {}: {}", nextFilename, lnr.getLineNumber()); 592 593 } catch (IOException e) { 594 throw new InternalErrorException(Msg.code(870) + e); 595 } finally { 596 IOUtils.closeQuietly(reader); 597 } 598 599 foundHlaNom = true; 600 } 601 602 if (IMGTHLA_HLA_XML.equals(nextFilename) || nextFilename.endsWith("/" + IMGTHLA_HLA_XML)) { 603 // process hla.xml file 604 ourLog.info("Processing file {}", nextFilename); 605 606 // IRecordHandler handler = new HlaXmlHandler(codeSystemVersion, code2concept, propertyNamesToTypes); 607 // AlleleSource alleleSource = new HlaXmlAlleleSource(hlaXmlFilename); 608 609 Reader reader = null; 610 try { 611 reader = new InputStreamReader(nextZipBytes.getInputStream(), Charsets.UTF_8); 612 613 LineNumberReader lnr = new LineNumberReader(reader); 614 while (lnr.readLine() != null) {} 615 ourLog.warn("Lines read from {}: {}", nextFilename, lnr.getLineNumber()); 616 617 } catch (IOException e) { 618 throw new InternalErrorException(Msg.code(871) + e); 619 } finally { 620 IOUtils.closeQuietly(reader); 621 } 622 623 foundHlaXml = true; 624 } 625 } 626 627 if (!foundHlaNom) { 628 throw new InvalidRequestException(Msg.code(872) + "Did not find file matching " + IMGTHLA_HLA_NOM_TXT); 629 } 630 631 if (!foundHlaXml) { 632 throw new InvalidRequestException(Msg.code(873) + "Did not find file matching " + IMGTHLA_HLA_XML); 633 } 634 635 int valueSetCount = valueSets.size(); 636 int rootConceptCount = codeSystemVersion.getConcepts().size(); 637 ourLog.info( 638 "Have {} total concepts, {} root concepts, {} ValueSets", 639 rootConceptCount, 640 rootConceptCount, 641 valueSetCount); 642 643 // remove this when fully implemented ... 644 throw new InternalErrorException( 645 Msg.code(874) + "HLA nomenclature terminology upload not yet fully implemented."); 646 647 // IIdType target = storeCodeSystem(theRequestDetails, codeSystemVersion, imgthlaCs, valueSets, conceptMaps); 648 // 649 // return new UploadStatistics(conceptCount, target); 650 } 651 652 UploadStatistics processLoincFiles( 653 LoadedFileDescriptors theDescriptors, 654 RequestDetails theRequestDetails, 655 Properties theUploadProperties, 656 Boolean theCloseFiles) { 657 final TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); 658 final Map<String, TermConcept> code2concept = new HashMap<>(); 659 final List<ValueSet> valueSets = new ArrayList<>(); 660 final List<ConceptMap> conceptMaps = new ArrayList<>(); 661 662 final List<LoincLinguisticVariantsHandler.LinguisticVariant> linguisticVariants = new ArrayList<>(); 663 664 LoincXmlFileZipContentsHandler loincXmlHandler = getLoincXmlFileZipContentsHandler(); 665 iterateOverZipFile(theDescriptors, "loinc.xml", false, false, loincXmlHandler); 666 String loincCsString = loincXmlHandler.getContents(); 667 if (isBlank(loincCsString)) { 668 throw new InvalidRequestException(Msg.code(875) + "Did not find loinc.xml in the ZIP distribution."); 669 } 670 671 CodeSystem loincCs = FhirContext.forR4Cached().newXmlParser().parseResource(CodeSystem.class, loincCsString); 672 if (isNotBlank(loincCs.getVersion())) { 673 throw new InvalidRequestException( 674 Msg.code(876) + "'loinc.xml' file must not have a version defined. To define a version use '" 675 + LOINC_CODESYSTEM_VERSION.getCode() + "' property of 'loincupload.properties' file"); 676 } 677 678 String codeSystemVersionId = theUploadProperties.getProperty(LOINC_CODESYSTEM_VERSION.getCode()); 679 if (codeSystemVersionId != null) { 680 loincCs.setVersion(codeSystemVersionId); 681 loincCs.setId(loincCs.getId() + "-" + codeSystemVersionId); 682 } 683 684 Map<String, CodeSystem.PropertyType> propertyNamesToTypes = new HashMap<>(); 685 for (CodeSystem.PropertyComponent nextProperty : loincCs.getProperty()) { 686 String nextPropertyCode = nextProperty.getCode(); 687 CodeSystem.PropertyType nextPropertyType = nextProperty.getType(); 688 if (isNotBlank(nextPropertyCode)) { 689 propertyNamesToTypes.put(nextPropertyCode, nextPropertyType); 690 } 691 } 692 693 // TODO: DM 2019-09-13 - Manually add EXTERNAL_COPYRIGHT_NOTICE property until Regenstrief adds this to 694 // loinc.xml 695 if (!propertyNamesToTypes.containsKey("EXTERNAL_COPYRIGHT_NOTICE")) { 696 String externalCopyRightNoticeCode = "EXTERNAL_COPYRIGHT_NOTICE"; 697 CodeSystem.PropertyType externalCopyRightNoticeType = CodeSystem.PropertyType.STRING; 698 propertyNamesToTypes.put(externalCopyRightNoticeCode, externalCopyRightNoticeType); 699 } 700 701 IZipContentsHandlerCsv handler; 702 703 // Part 704 handler = new LoincPartHandler(codeSystemVersion, code2concept); 705 iterateOverZipFileCsv( 706 theDescriptors, 707 theUploadProperties.getProperty(LOINC_PART_FILE.getCode(), LOINC_PART_FILE_DEFAULT.getCode()), 708 handler, 709 ',', 710 QuoteMode.NON_NUMERIC, 711 false); 712 Map<PartTypeAndPartName, String> partTypeAndPartNameToPartNumber = 713 ((LoincPartHandler) handler).getPartTypeAndPartNameToPartNumber(); 714 715 // LOINC string properties 716 handler = new LoincHandler( 717 codeSystemVersion, code2concept, propertyNamesToTypes, partTypeAndPartNameToPartNumber); 718 iterateOverZipFileCsv( 719 theDescriptors, 720 theUploadProperties.getProperty(LOINC_FILE.getCode(), LOINC_FILE_DEFAULT.getCode()), 721 handler, 722 ',', 723 QuoteMode.NON_NUMERIC, 724 false); 725 726 // LOINC hierarchy 727 handler = new LoincHierarchyHandler(codeSystemVersion, code2concept); 728 iterateOverZipFileCsv( 729 theDescriptors, 730 theUploadProperties.getProperty(LOINC_HIERARCHY_FILE.getCode(), LOINC_HIERARCHY_FILE_DEFAULT.getCode()), 731 handler, 732 ',', 733 QuoteMode.NON_NUMERIC, 734 false); 735 736 // Answer lists (ValueSets of potential answers/values for LOINC "questions") 737 handler = new LoincAnswerListHandler( 738 codeSystemVersion, code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 739 iterateOverZipFileCsv( 740 theDescriptors, 741 theUploadProperties.getProperty( 742 LOINC_ANSWERLIST_FILE.getCode(), LOINC_ANSWERLIST_FILE_DEFAULT.getCode()), 743 handler, 744 ',', 745 QuoteMode.NON_NUMERIC, 746 false); 747 748 // Answer list links (connects LOINC observation codes to answer list codes) 749 handler = new LoincAnswerListLinkHandler(code2concept); 750 iterateOverZipFileCsv( 751 theDescriptors, 752 theUploadProperties.getProperty( 753 LOINC_ANSWERLIST_LINK_FILE.getCode(), LOINC_ANSWERLIST_LINK_FILE_DEFAULT.getCode()), 754 handler, 755 ',', 756 QuoteMode.NON_NUMERIC, 757 false); 758 759 // RSNA playbook 760 // Note that this should come before the "Part Related Code Mapping" 761 // file because there are some duplicate mappings between these 762 // two files, and the RSNA Playbook file has more metadata 763 handler = new LoincRsnaPlaybookHandler( 764 code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 765 iterateOverZipFileCsv( 766 theDescriptors, 767 theUploadProperties.getProperty( 768 LOINC_RSNA_PLAYBOOK_FILE.getCode(), LOINC_RSNA_PLAYBOOK_FILE_DEFAULT.getCode()), 769 handler, 770 ',', 771 QuoteMode.NON_NUMERIC, 772 false); 773 774 // Part related code mapping 775 handler = new LoincPartRelatedCodeMappingHandler( 776 code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 777 iterateOverZipFileCsv( 778 theDescriptors, 779 theUploadProperties.getProperty( 780 LOINC_PART_RELATED_CODE_MAPPING_FILE.getCode(), 781 LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT.getCode()), 782 handler, 783 ',', 784 QuoteMode.NON_NUMERIC, 785 false); 786 787 // Document ontology 788 handler = new LoincDocumentOntologyHandler( 789 code2concept, 790 propertyNamesToTypes, 791 valueSets, 792 conceptMaps, 793 theUploadProperties, 794 loincCs.getCopyright()); 795 iterateOverZipFileCsv( 796 theDescriptors, 797 theUploadProperties.getProperty( 798 LOINC_DOCUMENT_ONTOLOGY_FILE.getCode(), LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT.getCode()), 799 handler, 800 ',', 801 QuoteMode.NON_NUMERIC, 802 false); 803 804 // Top 2000 codes - US 805 handler = new LoincTop2000LabResultsUsHandler( 806 code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 807 iterateOverZipFileCsvOptional( 808 theDescriptors, 809 theUploadProperties.getProperty( 810 LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE.getCode(), 811 LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT.getCode()), 812 handler, 813 ',', 814 QuoteMode.NON_NUMERIC, 815 false); 816 817 // Top 2000 codes - SI 818 handler = new LoincTop2000LabResultsSiHandler( 819 code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 820 iterateOverZipFileCsvOptional( 821 theDescriptors, 822 theUploadProperties.getProperty( 823 LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE.getCode(), 824 LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT.getCode()), 825 handler, 826 ',', 827 QuoteMode.NON_NUMERIC, 828 false); 829 830 // Universal lab order ValueSet 831 handler = new LoincUniversalOrderSetHandler(code2concept, valueSets, conceptMaps, theUploadProperties); 832 iterateOverZipFileCsv( 833 theDescriptors, 834 theUploadProperties.getProperty( 835 LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE.getCode(), 836 LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT.getCode()), 837 handler, 838 ',', 839 QuoteMode.NON_NUMERIC, 840 false); 841 842 // IEEE medical device codes 843 handler = new LoincIeeeMedicalDeviceCodeHandler( 844 code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 845 iterateOverZipFileCsv( 846 theDescriptors, 847 theUploadProperties.getProperty( 848 LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE.getCode(), 849 LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT.getCode()), 850 handler, 851 ',', 852 QuoteMode.NON_NUMERIC, 853 false); 854 855 // Imaging document codes 856 handler = new LoincImagingDocumentCodeHandler(code2concept, valueSets, conceptMaps, theUploadProperties); 857 iterateOverZipFileCsv( 858 theDescriptors, 859 theUploadProperties.getProperty( 860 LOINC_IMAGING_DOCUMENT_CODES_FILE.getCode(), 861 LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT.getCode()), 862 handler, 863 ',', 864 QuoteMode.NON_NUMERIC, 865 false); 866 867 // Group 868 handler = new LoincGroupFileHandler( 869 code2concept, valueSets, conceptMaps, theUploadProperties, loincCs.getCopyright()); 870 iterateOverZipFileCsv( 871 theDescriptors, 872 theUploadProperties.getProperty(LOINC_GROUP_FILE.getCode(), LOINC_GROUP_FILE_DEFAULT.getCode()), 873 handler, 874 ',', 875 QuoteMode.NON_NUMERIC, 876 false); 877 878 // Group terms 879 handler = new LoincGroupTermsFileHandler(code2concept, valueSets, conceptMaps, theUploadProperties); 880 iterateOverZipFileCsv( 881 theDescriptors, 882 theUploadProperties.getProperty( 883 LOINC_GROUP_TERMS_FILE.getCode(), LOINC_GROUP_TERMS_FILE_DEFAULT.getCode()), 884 handler, 885 ',', 886 QuoteMode.NON_NUMERIC, 887 false); 888 889 // Parent group 890 handler = new LoincParentGroupFileHandler(code2concept, valueSets, conceptMaps, theUploadProperties); 891 iterateOverZipFileCsv( 892 theDescriptors, 893 theUploadProperties.getProperty( 894 LOINC_PARENT_GROUP_FILE.getCode(), LOINC_PARENT_GROUP_FILE_DEFAULT.getCode()), 895 handler, 896 ',', 897 QuoteMode.NON_NUMERIC, 898 false); 899 900 // Part link 901 handler = new LoincPartLinkHandler(codeSystemVersion, code2concept, propertyNamesToTypes); 902 iterateOverZipFileCsvOptional( 903 theDescriptors, 904 theUploadProperties.getProperty(LOINC_PART_LINK_FILE.getCode(), LOINC_PART_LINK_FILE_DEFAULT.getCode()), 905 handler, 906 ',', 907 QuoteMode.NON_NUMERIC, 908 false); 909 iterateOverZipFileCsvOptional( 910 theDescriptors, 911 theUploadProperties.getProperty( 912 LOINC_PART_LINK_FILE_PRIMARY.getCode(), LOINC_PART_LINK_FILE_PRIMARY_DEFAULT.getCode()), 913 handler, 914 ',', 915 QuoteMode.NON_NUMERIC, 916 false); 917 iterateOverZipFileCsvOptional( 918 theDescriptors, 919 theUploadProperties.getProperty( 920 LOINC_PART_LINK_FILE_SUPPLEMENTARY.getCode(), 921 LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT.getCode()), 922 handler, 923 ',', 924 QuoteMode.NON_NUMERIC, 925 false); 926 927 // Consumer Name 928 handler = new LoincConsumerNameHandler(code2concept); 929 iterateOverZipFileCsvOptional( 930 theDescriptors, 931 theUploadProperties.getProperty( 932 LOINC_CONSUMER_NAME_FILE.getCode(), LOINC_CONSUMER_NAME_FILE_DEFAULT.getCode()), 933 handler, 934 ',', 935 QuoteMode.NON_NUMERIC, 936 false); 937 938 // LOINC coding properties (must run after all TermConcepts were created) 939 handler = new LoincCodingPropertiesHandler(code2concept, propertyNamesToTypes); 940 iterateOverZipFileCsv( 941 theDescriptors, 942 theUploadProperties.getProperty(LOINC_FILE.getCode(), LOINC_FILE_DEFAULT.getCode()), 943 handler, 944 ',', 945 QuoteMode.NON_NUMERIC, 946 false); 947 948 // Linguistic Variants 949 handler = new LoincLinguisticVariantsHandler(linguisticVariants); 950 iterateOverZipFileCsvOptional( 951 theDescriptors, 952 theUploadProperties.getProperty( 953 LOINC_LINGUISTIC_VARIANTS_FILE.getCode(), LOINC_LINGUISTIC_VARIANTS_FILE_DEFAULT.getCode()), 954 handler, 955 ',', 956 QuoteMode.NON_NUMERIC, 957 false); 958 959 String langFileName; 960 for (LoincLinguisticVariantsHandler.LinguisticVariant linguisticVariant : linguisticVariants) { 961 handler = new LoincLinguisticVariantHandler(code2concept, linguisticVariant.getLanguageCode()); 962 langFileName = linguisticVariant.getLinguisticVariantFileName(); 963 iterateOverZipFileCsvOptional( 964 theDescriptors, 965 theUploadProperties.getProperty( 966 LOINC_LINGUISTIC_VARIANTS_PATH.getCode() + langFileName, 967 LOINC_LINGUISTIC_VARIANTS_PATH_DEFAULT.getCode() + langFileName), 968 handler, 969 ',', 970 QuoteMode.NON_NUMERIC, 971 false); 972 } 973 974 if (theDescriptors.isOptionalFilesExist(List.of( 975 theUploadProperties.getProperty(LOINC_MAPTO_FILE.getCode(), LOINC_MAPTO_FILE_DEFAULT.getCode())))) { 976 // LOINC MapTo codes (last to make sure that all concepts were added to code2concept map) 977 handler = new LoincMapToHandler(code2concept); 978 iterateOverZipFileCsv( 979 theDescriptors, 980 theUploadProperties.getProperty(LOINC_MAPTO_FILE.getCode(), LOINC_MAPTO_FILE_DEFAULT.getCode()), 981 handler, 982 ',', 983 QuoteMode.NON_NUMERIC, 984 false); 985 } 986 987 if (theCloseFiles) { 988 IOUtils.closeQuietly(theDescriptors); 989 } 990 991 valueSets.add(getValueSetLoincAll(theUploadProperties, loincCs.getCopyright())); 992 993 for (Entry<String, TermConcept> next : code2concept.entrySet()) { 994 TermConcept nextConcept = next.getValue(); 995 if (nextConcept.getParents().isEmpty()) { 996 codeSystemVersion.getConcepts().add(nextConcept); 997 } 998 } 999 1000 int valueSetCount = valueSets.size(); 1001 int rootConceptCount = codeSystemVersion.getConcepts().size(); 1002 int conceptCount = code2concept.size(); 1003 ourLog.info( 1004 "Have {} total concepts, {} root concepts, {} ValueSets", 1005 conceptCount, 1006 rootConceptCount, 1007 valueSetCount); 1008 1009 IIdType target = storeCodeSystem(theRequestDetails, codeSystemVersion, loincCs, valueSets, conceptMaps); 1010 1011 return new UploadStatistics(conceptCount, target); 1012 } 1013 1014 @VisibleForTesting 1015 protected LoincXmlFileZipContentsHandler getLoincXmlFileZipContentsHandler() { 1016 return new LoincXmlFileZipContentsHandler(); 1017 } 1018 1019 private ValueSet getValueSetLoincAll(Properties theUploadProperties, String theCopyrightStatement) { 1020 ValueSet retVal = new ValueSet(); 1021 1022 String codeSystemVersionId = theUploadProperties.getProperty(LOINC_CODESYSTEM_VERSION.getCode()); 1023 String valueSetId; 1024 if (codeSystemVersionId != null) { 1025 valueSetId = LOINC_ALL_VALUESET_ID + "-" + codeSystemVersionId; 1026 } else { 1027 valueSetId = LOINC_ALL_VALUESET_ID; 1028 } 1029 retVal.setId(valueSetId); 1030 retVal.setUrl(LOINC_GENERIC_VALUESET_URL); 1031 retVal.setVersion(codeSystemVersionId); 1032 retVal.setName("All LOINC codes"); 1033 retVal.setStatus(Enumerations.PublicationStatus.ACTIVE); 1034 retVal.setDate(new Date()); 1035 retVal.setPublisher("Regenstrief Institute, Inc."); 1036 retVal.setDescription("A value set that includes all LOINC codes"); 1037 retVal.setCopyright(theCopyrightStatement); 1038 retVal.getCompose().addInclude().setSystem(ITermLoaderSvc.LOINC_URI).setVersion(codeSystemVersionId); 1039 1040 return retVal; 1041 } 1042 1043 private UploadStatistics processSnomedCtFiles( 1044 LoadedFileDescriptors theDescriptors, RequestDetails theRequestDetails) { 1045 final TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); 1046 final Map<String, TermConcept> id2concept = new HashMap<>(); 1047 final Map<String, TermConcept> code2concept = new HashMap<>(); 1048 final Set<String> validConceptIds = new HashSet<>(); 1049 1050 IZipContentsHandlerCsv handler = new SctHandlerConcept(validConceptIds); 1051 iterateOverZipFileCsv(theDescriptors, SCT_FILE_CONCEPT, handler, '\t', null, true); 1052 1053 ourLog.info("Have {} valid concept IDs", validConceptIds.size()); 1054 1055 handler = new SctHandlerDescription(validConceptIds, code2concept, id2concept, codeSystemVersion); 1056 iterateOverZipFileCsv(theDescriptors, SCT_FILE_DESCRIPTION, handler, '\t', null, true); 1057 1058 ourLog.info("Got {} concepts, cloning map", code2concept.size()); 1059 final HashMap<String, TermConcept> rootConcepts = new HashMap<>(code2concept); 1060 1061 handler = new SctHandlerRelationship(codeSystemVersion, rootConcepts, code2concept); 1062 iterateOverZipFileCsv(theDescriptors, SCT_FILE_RELATIONSHIP, handler, '\t', null, true); 1063 1064 IOUtils.closeQuietly(theDescriptors); 1065 1066 ourLog.info("Looking for root codes"); 1067 rootConcepts 1068 .entrySet() 1069 .removeIf(theStringTermConceptEntry -> 1070 !theStringTermConceptEntry.getValue().getParents().isEmpty()); 1071 1072 ourLog.info( 1073 "Done loading SNOMED CT files - {} root codes, {} total codes", 1074 rootConcepts.size(), 1075 code2concept.size()); 1076 1077 Counter circularCounter = new Counter(); 1078 for (TermConcept next : rootConcepts.values()) { 1079 long count = circularCounter.getThenAdd(); 1080 float pct = ((float) count / rootConcepts.size()) * 100.0f; 1081 ourLog.info( 1082 " * Scanning for circular refs - have scanned {} / {} codes ({}%)", 1083 count, rootConcepts.size(), pct); 1084 dropCircularRefs(next, new ArrayList<>(), code2concept); 1085 } 1086 1087 codeSystemVersion.getConcepts().addAll(rootConcepts.values()); 1088 1089 CodeSystem cs = new org.hl7.fhir.r4.model.CodeSystem(); 1090 cs.setUrl(SCT_URI); 1091 cs.setName("SNOMED CT"); 1092 cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); 1093 cs.setStatus(Enumerations.PublicationStatus.ACTIVE); 1094 IIdType target = storeCodeSystem(theRequestDetails, codeSystemVersion, cs, null, null); 1095 1096 return new UploadStatistics(code2concept.size(), target); 1097 } 1098 1099 private IIdType storeCodeSystem( 1100 RequestDetails theRequestDetails, 1101 final TermCodeSystemVersion theCodeSystemVersion, 1102 CodeSystem theCodeSystem, 1103 List<ValueSet> theValueSets, 1104 List<ConceptMap> theConceptMaps) { 1105 Validate.isTrue(theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT); 1106 1107 List<ValueSet> valueSets = ObjectUtils.defaultIfNull(theValueSets, Collections.emptyList()); 1108 List<ConceptMap> conceptMaps = ObjectUtils.defaultIfNull(theConceptMaps, Collections.emptyList()); 1109 1110 IIdType retVal; 1111 myDeferredStorageSvc.setProcessDeferred(false); 1112 retVal = myCodeSystemStorageSvc.storeNewCodeSystemVersion( 1113 theCodeSystem, theCodeSystemVersion, theRequestDetails, valueSets, conceptMaps); 1114 myDeferredStorageSvc.setProcessDeferred(true); 1115 1116 return retVal; 1117 } 1118 1119 public static void iterateOverZipFileCsv( 1120 LoadedFileDescriptors theDescriptors, 1121 String theFileNamePart, 1122 IZipContentsHandlerCsv theHandler, 1123 char theDelimiter, 1124 QuoteMode theQuoteMode, 1125 boolean theIsPartialFilename) { 1126 iterateOverZipFileCsv( 1127 theDescriptors, theFileNamePart, theHandler, theDelimiter, theQuoteMode, theIsPartialFilename, true); 1128 } 1129 1130 public static void iterateOverZipFileCsvOptional( 1131 LoadedFileDescriptors theDescriptors, 1132 String theFileNamePart, 1133 IZipContentsHandlerCsv theHandler, 1134 char theDelimiter, 1135 QuoteMode theQuoteMode, 1136 boolean theIsPartialFilename) { 1137 iterateOverZipFileCsv( 1138 theDescriptors, theFileNamePart, theHandler, theDelimiter, theQuoteMode, theIsPartialFilename, false); 1139 } 1140 1141 private static void iterateOverZipFileCsv( 1142 LoadedFileDescriptors theDescriptors, 1143 String theFileNamePart, 1144 IZipContentsHandlerCsv theHandler, 1145 char theDelimiter, 1146 QuoteMode theQuoteMode, 1147 boolean theIsPartialFilename, 1148 boolean theRequireMatch) { 1149 IZipContentsHandler handler = (reader, filename) -> { 1150 CSVParser parsed = newCsvRecords(theDelimiter, theQuoteMode, reader); 1151 Iterator<CSVRecord> iter = parsed.iterator(); 1152 ourLog.debug("Header map: {}", parsed.getHeaderMap()); 1153 1154 int count = 0; 1155 int nextLoggedCount = 0; 1156 while (iter.hasNext()) { 1157 CSVRecord nextRecord = iter.next(); 1158 if (!nextRecord.isConsistent()) { 1159 continue; 1160 } 1161 theHandler.accept(nextRecord); 1162 count++; 1163 if (count >= nextLoggedCount) { 1164 ourLog.info(" * Processed {} records in {}", count, filename); 1165 nextLoggedCount += LOG_INCREMENT; 1166 } 1167 } 1168 }; 1169 1170 iterateOverZipFile(theDescriptors, theFileNamePart, theIsPartialFilename, theRequireMatch, handler); 1171 } 1172 1173 private static void iterateOverZipFile( 1174 LoadedFileDescriptors theDescriptors, 1175 String theFileNamePart, 1176 boolean theIsPartialFilename, 1177 boolean theRequireMatch, 1178 IZipContentsHandler theHandler) { 1179 boolean foundMatch = false; 1180 for (FileDescriptor nextZipBytes : theDescriptors.getUncompressedFileDescriptors()) { 1181 String nextFilename = nextZipBytes.getFilename(); 1182 boolean matches; 1183 if (theIsPartialFilename) { 1184 matches = nextFilename.contains(theFileNamePart); 1185 } else { 1186 matches = nextFilename.endsWith("/" + theFileNamePart) || nextFilename.equals(theFileNamePart); 1187 } 1188 1189 if (matches) { 1190 ourLog.info("Processing file {}", nextFilename); 1191 foundMatch = true; 1192 1193 try { 1194 1195 Reader reader = new InputStreamReader(nextZipBytes.getInputStream(), Charsets.UTF_8); 1196 theHandler.handle(reader, nextFilename); 1197 1198 } catch (IOException e) { 1199 throw new InternalErrorException(Msg.code(877) + e); 1200 } 1201 } 1202 } 1203 1204 if (!foundMatch && theRequireMatch) { 1205 throw new InvalidRequestException(Msg.code(878) + "Did not find file matching " + theFileNamePart); 1206 } 1207 } 1208 1209 @Nonnull 1210 private static CSVParser newCsvRecords(char theDelimiter, QuoteMode theQuoteMode, Reader theReader) 1211 throws IOException { 1212 CSVParser parsed; 1213 CSVFormat format = 1214 CSVFormat.newFormat(theDelimiter).withFirstRecordAsHeader().withTrim(); 1215 if (theQuoteMode != null) { 1216 format = format.withQuote('"').withQuoteMode(theQuoteMode); 1217 } 1218 parsed = new CSVParser(theReader, format); 1219 return parsed; 1220 } 1221 1222 public static String firstNonBlank(String... theStrings) { 1223 String retVal = ""; 1224 for (String nextString : theStrings) { 1225 if (isNotBlank(nextString)) { 1226 retVal = nextString; 1227 break; 1228 } 1229 } 1230 return retVal; 1231 } 1232 1233 public static TermConcept getOrCreateConcept(Map<String, TermConcept> id2concept, String id) { 1234 TermConcept concept = id2concept.get(id); 1235 if (concept == null) { 1236 concept = new TermConcept(); 1237 id2concept.put(id, concept); 1238 } 1239 return concept; 1240 } 1241 1242 public static TermConceptProperty getOrCreateConceptProperty( 1243 Map<String, List<TermConceptProperty>> code2Properties, String code, String key) { 1244 List<TermConceptProperty> termConceptProperties = code2Properties.get(code); 1245 if (termConceptProperties == null) return new TermConceptProperty(); 1246 Optional<TermConceptProperty> termConceptProperty = termConceptProperties.stream() 1247 .filter(property -> key.equals(property.getKey())) 1248 .findFirst(); 1249 return termConceptProperty.orElseGet(TermConceptProperty::new); 1250 } 1251}