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.provider; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.jpa.model.util.JpaConstants; 026import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; 027import ca.uhn.fhir.jpa.term.UploadStatistics; 028import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; 029import ca.uhn.fhir.jpa.term.custom.ConceptHandler; 030import ca.uhn.fhir.jpa.term.custom.HierarchyHandler; 031import ca.uhn.fhir.jpa.term.custom.PropertyHandler; 032import ca.uhn.fhir.rest.annotation.Operation; 033import ca.uhn.fhir.rest.annotation.OperationParam; 034import ca.uhn.fhir.rest.api.server.RequestDetails; 035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 037import ca.uhn.fhir.util.AttachmentUtil; 038import ca.uhn.fhir.util.ParametersUtil; 039import ca.uhn.fhir.util.ValidateUtil; 040import com.google.common.base.Charsets; 041import com.google.common.collect.ArrayListMultimap; 042import com.google.common.collect.Multimap; 043import jakarta.annotation.Nonnull; 044import jakarta.servlet.http.HttpServletRequest; 045import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_30_40; 046import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50; 047import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_40; 048import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; 049import org.hl7.fhir.instance.model.api.IBaseParameters; 050import org.hl7.fhir.instance.model.api.IBaseResource; 051import org.hl7.fhir.instance.model.api.ICompositeType; 052import org.hl7.fhir.instance.model.api.IPrimitiveType; 053import org.hl7.fhir.r4.model.CodeSystem; 054import org.springframework.beans.factory.annotation.Autowired; 055 056import java.io.File; 057import java.io.FileInputStream; 058import java.io.FileNotFoundException; 059import java.io.InputStream; 060import java.util.ArrayList; 061import java.util.LinkedHashMap; 062import java.util.List; 063import java.util.Map; 064 065import static org.apache.commons.lang3.StringUtils.*; 066 067public class TerminologyUploaderProvider extends BaseJpaProvider { 068 069 public static final String PARAM_FILE = "file"; 070 public static final String PARAM_CODESYSTEM = "codeSystem"; 071 public static final String PARAM_SYSTEM = "system"; 072 private static final String RESP_PARAM_CONCEPT_COUNT = "conceptCount"; 073 private static final String RESP_PARAM_TARGET = "target"; 074 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TerminologyUploaderProvider.class); 075 private static final String RESP_PARAM_SUCCESS = "success"; 076 077 @Autowired 078 private ITermLoaderSvc myTerminologyLoaderSvc; 079 080 /** 081 * Constructor 082 */ 083 public TerminologyUploaderProvider() { 084 this(null, null); 085 } 086 087 /** 088 * Constructor 089 */ 090 public TerminologyUploaderProvider(FhirContext theContext, ITermLoaderSvc theTerminologyLoaderSvc) { 091 setContext(theContext); 092 myTerminologyLoaderSvc = theTerminologyLoaderSvc; 093 } 094 095 /** 096 * <code> 097 * $upload-external-codesystem 098 * </code> 099 */ 100 @Operation( 101 typeName = "CodeSystem", 102 name = JpaConstants.OPERATION_UPLOAD_EXTERNAL_CODE_SYSTEM, 103 idempotent = false, 104 returnParameters = { 105 // @OperationParam(name = "conceptCount", type = IntegerType.class, min = 1) 106 }) 107 public IBaseParameters uploadSnapshot( 108 HttpServletRequest theServletRequest, 109 @OperationParam(name = PARAM_SYSTEM, min = 1, typeName = "uri") IPrimitiveType<String> theCodeSystemUrl, 110 @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment") 111 List<ICompositeType> theFiles, 112 RequestDetails theRequestDetails) { 113 114 startRequest(theServletRequest); 115 116 if (theCodeSystemUrl == null || isBlank(theCodeSystemUrl.getValueAsString())) { 117 throw new InvalidRequestException(Msg.code(1137) + "Missing mandatory parameter: " + PARAM_SYSTEM); 118 } 119 120 if (theFiles == null || theFiles.size() == 0) { 121 throw new InvalidRequestException( 122 Msg.code(1138) + "No '" + PARAM_FILE + "' parameter, or package had no data"); 123 } 124 for (ICompositeType next : theFiles) { 125 ValidateUtil.isTrueOrThrowInvalidRequest( 126 getContext().getElementDefinition(next.getClass()).getName().equals("Attachment"), 127 "Package must be of type Attachment"); 128 } 129 130 try { 131 List<ITermLoaderSvc.FileDescriptor> localFiles = convertAttachmentsToFileDescriptors(theFiles); 132 133 String codeSystemUrl = theCodeSystemUrl.getValue(); 134 codeSystemUrl = trim(codeSystemUrl); 135 136 UploadStatistics stats; 137 switch (codeSystemUrl) { 138 case ITermLoaderSvc.ICD10_URI: 139 stats = myTerminologyLoaderSvc.loadIcd10(localFiles, theRequestDetails); 140 break; 141 case ITermLoaderSvc.ICD10CM_URI: 142 stats = myTerminologyLoaderSvc.loadIcd10cm(localFiles, theRequestDetails); 143 break; 144 case ITermLoaderSvc.IMGTHLA_URI: 145 stats = myTerminologyLoaderSvc.loadImgthla(localFiles, theRequestDetails); 146 break; 147 case ITermLoaderSvc.LOINC_URI: 148 stats = myTerminologyLoaderSvc.loadLoinc(localFiles, theRequestDetails); 149 break; 150 case ITermLoaderSvc.SCT_URI: 151 stats = myTerminologyLoaderSvc.loadSnomedCt(localFiles, theRequestDetails); 152 break; 153 default: 154 stats = myTerminologyLoaderSvc.loadCustom(codeSystemUrl, localFiles, theRequestDetails); 155 break; 156 } 157 158 IBaseParameters retVal = ParametersUtil.newInstance(getContext()); 159 ParametersUtil.addParameterToParametersBoolean(getContext(), retVal, RESP_PARAM_SUCCESS, true); 160 ParametersUtil.addParameterToParametersInteger( 161 getContext(), retVal, RESP_PARAM_CONCEPT_COUNT, stats.getUpdatedConceptCount()); 162 ParametersUtil.addParameterToParametersReference( 163 getContext(), retVal, RESP_PARAM_TARGET, stats.getTarget().getValue()); 164 165 return retVal; 166 } finally { 167 endRequest(theServletRequest); 168 } 169 } 170 171 /** 172 * <code> 173 * $apply-codesystem-delta-add 174 * </code> 175 */ 176 @Operation( 177 typeName = "CodeSystem", 178 name = JpaConstants.OPERATION_APPLY_CODESYSTEM_DELTA_ADD, 179 idempotent = false, 180 returnParameters = {}) 181 public IBaseParameters uploadDeltaAdd( 182 HttpServletRequest theServletRequest, 183 @OperationParam(name = PARAM_SYSTEM, min = 1, max = 1, typeName = "uri") IPrimitiveType<String> theSystem, 184 @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment") 185 List<ICompositeType> theFiles, 186 @OperationParam( 187 name = PARAM_CODESYSTEM, 188 min = 0, 189 max = OperationParam.MAX_UNLIMITED, 190 typeName = "CodeSystem") 191 List<IBaseResource> theCodeSystems, 192 RequestDetails theRequestDetails) { 193 194 startRequest(theServletRequest); 195 try { 196 validateHaveSystem(theSystem); 197 validateHaveFiles(theFiles, theCodeSystems); 198 199 List<ITermLoaderSvc.FileDescriptor> files = convertAttachmentsToFileDescriptors(theFiles); 200 convertCodeSystemsToFileDescriptors(files, theCodeSystems); 201 UploadStatistics outcome = 202 myTerminologyLoaderSvc.loadDeltaAdd(theSystem.getValue(), files, theRequestDetails); 203 return toDeltaResponse(outcome); 204 } finally { 205 endRequest(theServletRequest); 206 } 207 } 208 209 /** 210 * <code> 211 * $apply-codesystem-delta-remove 212 * </code> 213 */ 214 @Operation( 215 typeName = "CodeSystem", 216 name = JpaConstants.OPERATION_APPLY_CODESYSTEM_DELTA_REMOVE, 217 idempotent = false, 218 returnParameters = {}) 219 public IBaseParameters uploadDeltaRemove( 220 HttpServletRequest theServletRequest, 221 @OperationParam(name = PARAM_SYSTEM, min = 1, max = 1, typeName = "uri") IPrimitiveType<String> theSystem, 222 @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment") 223 List<ICompositeType> theFiles, 224 @OperationParam( 225 name = PARAM_CODESYSTEM, 226 min = 0, 227 max = OperationParam.MAX_UNLIMITED, 228 typeName = "CodeSystem") 229 List<IBaseResource> theCodeSystems, 230 RequestDetails theRequestDetails) { 231 232 startRequest(theServletRequest); 233 try { 234 validateHaveSystem(theSystem); 235 validateHaveFiles(theFiles, theCodeSystems); 236 237 List<ITermLoaderSvc.FileDescriptor> files = convertAttachmentsToFileDescriptors(theFiles); 238 convertCodeSystemsToFileDescriptors(files, theCodeSystems); 239 UploadStatistics outcome = 240 myTerminologyLoaderSvc.loadDeltaRemove(theSystem.getValue(), files, theRequestDetails); 241 return toDeltaResponse(outcome); 242 } finally { 243 endRequest(theServletRequest); 244 } 245 } 246 247 private void convertCodeSystemsToFileDescriptors( 248 List<ITermLoaderSvc.FileDescriptor> theFiles, List<IBaseResource> theCodeSystems) { 249 Map<String, String> codes = new LinkedHashMap<>(); 250 Map<String, List<CodeSystem.ConceptPropertyComponent>> codeToProperties = new LinkedHashMap<>(); 251 252 Multimap<String, String> codeToParentCodes = ArrayListMultimap.create(); 253 254 if (theCodeSystems != null) { 255 for (IBaseResource nextCodeSystemUncast : theCodeSystems) { 256 CodeSystem nextCodeSystem = canonicalizeCodeSystem(nextCodeSystemUncast); 257 convertCodeSystemCodesToCsv( 258 nextCodeSystem.getConcept(), codes, codeToProperties, null, codeToParentCodes); 259 } 260 } 261 262 // Create concept file 263 if (codes.size() > 0) { 264 StringBuilder b = new StringBuilder(); 265 b.append(ConceptHandler.CODE); 266 b.append(","); 267 b.append(ConceptHandler.DISPLAY); 268 b.append("\n"); 269 for (Map.Entry<String, String> nextEntry : codes.entrySet()) { 270 b.append(csvEscape(nextEntry.getKey())); 271 b.append(","); 272 b.append(csvEscape(nextEntry.getValue())); 273 b.append("\n"); 274 } 275 byte[] bytes = b.toString().getBytes(Charsets.UTF_8); 276 String fileName = TermLoaderSvcImpl.CUSTOM_CONCEPTS_FILE; 277 ITermLoaderSvc.ByteArrayFileDescriptor fileDescriptor = 278 new ITermLoaderSvc.ByteArrayFileDescriptor(fileName, bytes); 279 theFiles.add(fileDescriptor); 280 } 281 282 // Create hierarchy file 283 if (codeToParentCodes.size() > 0) { 284 StringBuilder b = new StringBuilder(); 285 b.append(HierarchyHandler.CHILD); 286 b.append(","); 287 b.append(HierarchyHandler.PARENT); 288 b.append("\n"); 289 for (Map.Entry<String, String> nextEntry : codeToParentCodes.entries()) { 290 b.append(csvEscape(nextEntry.getKey())); 291 b.append(","); 292 b.append(csvEscape(nextEntry.getValue())); 293 b.append("\n"); 294 } 295 byte[] bytes = b.toString().getBytes(Charsets.UTF_8); 296 String fileName = TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE; 297 ITermLoaderSvc.ByteArrayFileDescriptor fileDescriptor = 298 new ITermLoaderSvc.ByteArrayFileDescriptor(fileName, bytes); 299 theFiles.add(fileDescriptor); 300 } 301 // Create codeToProperties file 302 if (codeToProperties.size() > 0) { 303 StringBuilder b = new StringBuilder(); 304 b.append(PropertyHandler.CODE); 305 b.append(","); 306 b.append(PropertyHandler.KEY); 307 b.append(","); 308 b.append(PropertyHandler.VALUE); 309 b.append(","); 310 b.append(PropertyHandler.TYPE); 311 b.append("\n"); 312 313 for (Map.Entry<String, List<CodeSystem.ConceptPropertyComponent>> nextEntry : codeToProperties.entrySet()) { 314 for (CodeSystem.ConceptPropertyComponent propertyComponent : nextEntry.getValue()) { 315 b.append(csvEscape(nextEntry.getKey())); 316 b.append(","); 317 b.append(csvEscape(propertyComponent.getCode())); 318 b.append(","); 319 // TODO: check this for different types, other types should be added once 320 // TermConceptPropertyTypeEnum contain different types 321 b.append(csvEscape(propertyComponent.getValueStringType().getValue())); 322 b.append(","); 323 b.append(csvEscape(propertyComponent.getValue().primitiveValue())); 324 b.append("\n"); 325 } 326 } 327 byte[] bytes = b.toString().getBytes(Charsets.UTF_8); 328 String fileName = TermLoaderSvcImpl.CUSTOM_PROPERTIES_FILE; 329 ITermLoaderSvc.ByteArrayFileDescriptor fileDescriptor = 330 new ITermLoaderSvc.ByteArrayFileDescriptor(fileName, bytes); 331 theFiles.add(fileDescriptor); 332 } 333 } 334 335 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 336 @Nonnull 337 CodeSystem canonicalizeCodeSystem(@Nonnull IBaseResource theCodeSystem) { 338 RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theCodeSystem); 339 ValidateUtil.isTrueOrThrowInvalidRequest( 340 resourceDef.getName().equals("CodeSystem"), "Resource '%s' is not a CodeSystem", resourceDef.getName()); 341 342 CodeSystem nextCodeSystem; 343 switch (getContext().getVersion().getVersion()) { 344 case DSTU3: 345 nextCodeSystem = (CodeSystem) VersionConvertorFactory_30_40.convertResource( 346 (org.hl7.fhir.dstu3.model.CodeSystem) theCodeSystem, new BaseAdvisor_30_40(false)); 347 break; 348 case R5: 349 nextCodeSystem = (CodeSystem) VersionConvertorFactory_40_50.convertResource( 350 (org.hl7.fhir.r5.model.CodeSystem) theCodeSystem, new BaseAdvisor_40_50(false)); 351 break; 352 default: 353 nextCodeSystem = (CodeSystem) theCodeSystem; 354 } 355 return nextCodeSystem; 356 } 357 358 private void convertCodeSystemCodesToCsv( 359 List<CodeSystem.ConceptDefinitionComponent> theConcept, 360 Map<String, String> theCodes, 361 Map<String, List<CodeSystem.ConceptPropertyComponent>> theProperties, 362 String theParentCode, 363 Multimap<String, String> theCodeToParentCodes) { 364 for (CodeSystem.ConceptDefinitionComponent nextConcept : theConcept) { 365 if (isNotBlank(nextConcept.getCode())) { 366 theCodes.put(nextConcept.getCode(), nextConcept.getDisplay()); 367 if (isNotBlank(theParentCode)) { 368 theCodeToParentCodes.put(nextConcept.getCode(), theParentCode); 369 } 370 if (nextConcept.getProperty() != null) { 371 theProperties.put(nextConcept.getCode(), nextConcept.getProperty()); 372 } 373 convertCodeSystemCodesToCsv( 374 nextConcept.getConcept(), theCodes, theProperties, nextConcept.getCode(), theCodeToParentCodes); 375 } 376 } 377 } 378 379 private void validateHaveSystem(IPrimitiveType<String> theSystem) { 380 if (theSystem == null || isBlank(theSystem.getValueAsString())) { 381 throw new InvalidRequestException(Msg.code(1139) + "Missing mandatory parameter: " + PARAM_SYSTEM); 382 } 383 } 384 385 private void validateHaveFiles(List<ICompositeType> theFiles, List<IBaseResource> theCodeSystems) { 386 if (theFiles != null) { 387 for (ICompositeType nextFile : theFiles) { 388 if (!nextFile.isEmpty()) { 389 return; 390 } 391 } 392 } 393 if (theCodeSystems != null) { 394 for (IBaseResource next : theCodeSystems) { 395 if (!next.isEmpty()) { 396 return; 397 } 398 } 399 } 400 throw new InvalidRequestException(Msg.code(1140) + "Missing mandatory parameter: " + PARAM_FILE); 401 } 402 403 @Nonnull 404 private List<ITermLoaderSvc.FileDescriptor> convertAttachmentsToFileDescriptors( 405 @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment") 406 List<ICompositeType> theFiles) { 407 List<ITermLoaderSvc.FileDescriptor> files = new ArrayList<>(); 408 if (theFiles != null) { 409 for (ICompositeType next : theFiles) { 410 411 String nextUrl = 412 AttachmentUtil.getOrCreateUrl(getContext(), next).getValue(); 413 ValidateUtil.isNotBlankOrThrowUnprocessableEntity(nextUrl, "Missing Attachment.url value"); 414 415 byte[] nextData; 416 if (nextUrl.startsWith("localfile:")) { 417 String nextLocalFile = nextUrl.substring("localfile:".length()); 418 419 if (isNotBlank(nextLocalFile)) { 420 ourLog.info("Reading in local file: {}", nextLocalFile); 421 File nextFile = new File(nextLocalFile); 422 if (!nextFile.exists() || !nextFile.isFile()) { 423 throw new InvalidRequestException(Msg.code(1141) + "Unknown file: " + nextFile.getName()); 424 } 425 files.add(new FileBackedFileDescriptor(nextFile)); 426 } 427 428 } else { 429 nextData = 430 AttachmentUtil.getOrCreateData(getContext(), next).getValue(); 431 ValidateUtil.isTrueOrThrowInvalidRequest( 432 nextData != null && nextData.length > 0, "Missing Attachment.data value"); 433 files.add(new ITermLoaderSvc.ByteArrayFileDescriptor(nextUrl, nextData)); 434 } 435 } 436 } 437 return files; 438 } 439 440 private IBaseParameters toDeltaResponse(UploadStatistics theOutcome) { 441 IBaseParameters retVal = ParametersUtil.newInstance(getContext()); 442 ParametersUtil.addParameterToParametersInteger( 443 getContext(), retVal, RESP_PARAM_CONCEPT_COUNT, theOutcome.getUpdatedConceptCount()); 444 ParametersUtil.addParameterToParametersReference( 445 getContext(), retVal, RESP_PARAM_TARGET, theOutcome.getTarget().getValue()); 446 return retVal; 447 } 448 449 public void setTerminologyLoaderSvc(ITermLoaderSvc theTermLoaderSvc) { 450 myTerminologyLoaderSvc = theTermLoaderSvc; 451 } 452 453 public static class FileBackedFileDescriptor implements ITermLoaderSvc.FileDescriptor { 454 private final File myNextFile; 455 456 public FileBackedFileDescriptor(File theNextFile) { 457 myNextFile = theNextFile; 458 } 459 460 @Override 461 public String getFilename() { 462 return myNextFile.getAbsolutePath(); 463 } 464 465 @Override 466 public InputStream getInputStream() { 467 try { 468 return new FileInputStream(myNextFile); 469 } catch (FileNotFoundException theE) { 470 throw new InternalErrorException(Msg.code(1142) + theE); 471 } 472 } 473 } 474 475 private static String csvEscape(String theValue) { 476 return '"' + theValue.replace("\"", "\"\"").replace("\n", "\\n").replace("\r", "") + '"'; 477 } 478}