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.context.FhirVersionEnum; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 027import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 028import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 029import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; 030import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 031import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; 032import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; 033import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; 034import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao; 035import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao; 036import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao; 037import ca.uhn.fhir.jpa.entity.TermCodeSystem; 038import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; 039import ca.uhn.fhir.jpa.entity.TermConcept; 040import ca.uhn.fhir.jpa.entity.TermConceptDesignation; 041import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; 042import ca.uhn.fhir.jpa.entity.TermConceptProperty; 043import ca.uhn.fhir.jpa.model.dao.JpaPid; 044import ca.uhn.fhir.jpa.model.entity.ResourceTable; 045import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; 046import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; 047import ca.uhn.fhir.jpa.term.api.ITermReadSvc; 048import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc; 049import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet; 050import ca.uhn.fhir.rest.api.Constants; 051import ca.uhn.fhir.rest.api.server.RequestDetails; 052import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 053import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 054import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 055import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 056import ca.uhn.fhir.util.ObjectUtil; 057import ca.uhn.fhir.util.UrlUtil; 058import ca.uhn.fhir.util.ValidateUtil; 059import jakarta.annotation.Nonnull; 060import jakarta.persistence.EntityManager; 061import jakarta.persistence.PersistenceContext; 062import jakarta.persistence.PersistenceContextType; 063import org.apache.commons.lang3.Validate; 064import org.hl7.fhir.instance.model.api.IIdType; 065import org.hl7.fhir.r4.model.CodeSystem; 066import org.hl7.fhir.r4.model.ConceptMap; 067import org.hl7.fhir.r4.model.ValueSet; 068import org.slf4j.Logger; 069import org.slf4j.LoggerFactory; 070import org.springframework.beans.factory.annotation.Autowired; 071import org.springframework.transaction.annotation.Propagation; 072import org.springframework.transaction.annotation.Transactional; 073import org.springframework.transaction.support.TransactionSynchronizationManager; 074 075import java.util.ArrayList; 076import java.util.Arrays; 077import java.util.Collection; 078import java.util.Collections; 079import java.util.HashMap; 080import java.util.HashSet; 081import java.util.IdentityHashMap; 082import java.util.List; 083import java.util.Map; 084import java.util.Objects; 085import java.util.Optional; 086import java.util.Set; 087import java.util.UUID; 088import java.util.concurrent.atomic.AtomicInteger; 089import java.util.stream.Collectors; 090 091import static ca.uhn.fhir.jpa.api.dao.IDao.RESOURCE_PID_KEY; 092import static org.apache.commons.lang3.StringUtils.isBlank; 093import static org.apache.commons.lang3.StringUtils.isNotBlank; 094import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW; 095 096public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { 097 private static final Logger ourLog = LoggerFactory.getLogger(TermCodeSystemStorageSvcImpl.class); 098 private static final Object PLACEHOLDER_OBJECT = new Object(); 099 100 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 101 protected EntityManager myEntityManager; 102 103 @Autowired 104 protected ITermCodeSystemDao myCodeSystemDao; 105 106 @Autowired 107 protected ITermCodeSystemVersionDao myCodeSystemVersionDao; 108 109 @Autowired 110 protected ITermConceptDao myConceptDao; 111 112 @Autowired 113 protected ITermConceptPropertyDao myConceptPropertyDao; 114 115 @Autowired 116 protected ITermConceptDesignationDao myConceptDesignationDao; 117 118 @Autowired 119 protected IIdHelperService<JpaPid> myIdHelperService; 120 121 @Autowired 122 private ITermConceptParentChildLinkDao myConceptParentChildLinkDao; 123 124 @Autowired 125 private ITermVersionAdapterSvc myTerminologyVersionAdapterSvc; 126 127 @Autowired 128 private ITermDeferredStorageSvc myDeferredStorageSvc; 129 130 @Autowired 131 private FhirContext myContext; 132 133 @Autowired 134 private ITermReadSvc myTerminologySvc; 135 136 @Autowired 137 private JpaStorageSettings myStorageSettings; 138 139 @Autowired 140 private IResourceTableDao myResourceTableDao; 141 142 @Autowired 143 private TermConceptDaoSvc myTermConceptDaoSvc; 144 145 @Transactional 146 @Override 147 public UploadStatistics applyDeltaCodeSystemsAdd(String theSystem, CustomTerminologySet theAdditions) { 148 ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystem, "No system provided"); 149 validateDstu3OrNewer(); 150 theAdditions.validateNoCycleOrThrowInvalidRequest(); 151 152 TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theSystem); 153 if (cs == null) { 154 CodeSystem codeSystemResource = new CodeSystem(); 155 codeSystemResource.setUrl(theSystem); 156 codeSystemResource.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); 157 if (isBlank(codeSystemResource.getIdElement().getIdPart()) && theSystem.contains(LOINC_LOW)) { 158 codeSystemResource.setId(LOINC_LOW); 159 } 160 myTerminologyVersionAdapterSvc.createOrUpdateCodeSystem(codeSystemResource); 161 162 cs = myCodeSystemDao.findByCodeSystemUri(theSystem); 163 } 164 165 TermCodeSystemVersion csv = cs.getCurrentVersion(); 166 Validate.notNull(csv); 167 168 CodeSystem codeSystem = myTerminologySvc.fetchCanonicalCodeSystemFromCompleteContext(theSystem); 169 if (codeSystem.getContent() != CodeSystem.CodeSystemContentMode.NOTPRESENT) { 170 throw new InvalidRequestException( 171 Msg.code(844) + "CodeSystem with url[" + Constants.codeSystemWithDefaultDescription(theSystem) 172 + "] can not apply a delta - wrong content mode: " + codeSystem.getContent()); 173 } 174 175 Validate.notNull(cs); 176 Validate.notNull(cs.getPid()); 177 178 IIdType codeSystemId = cs.getResource().getIdDt(); 179 180 UploadStatistics retVal = new UploadStatistics(codeSystemId); 181 HashMap<String, TermConcept> codeToConcept = new HashMap<>(); 182 183 // Add root concepts 184 for (TermConcept nextRootConcept : theAdditions.getRootConcepts()) { 185 List<String> parentCodes = Collections.emptyList(); 186 addConceptInHierarchy(csv, parentCodes, nextRootConcept, retVal, codeToConcept, 0); 187 } 188 189 return retVal; 190 } 191 192 @Transactional 193 @Override 194 public UploadStatistics applyDeltaCodeSystemsRemove(String theSystem, CustomTerminologySet theValue) { 195 ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystem, "No system provided"); 196 validateDstu3OrNewer(); 197 198 TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theSystem); 199 if (cs == null) { 200 throw new InvalidRequestException(Msg.code(845) + "Unknown code system: " + theSystem); 201 } 202 IIdType target = cs.getResource().getIdDt(); 203 204 AtomicInteger removeCounter = new AtomicInteger(0); 205 206 // We need to delete all termconcepts, and their children. This stream flattens the TermConcepts and their 207 // children into a single set of TermConcept objects retrieved from the DB. Note that we have to do this because 208 // deleteById() in JPA doesnt appear to actually commit or flush a transaction until way later, and we end up 209 // iterating multiple times over the same elements, which screws up our counter. 210 211 // Grab the actual entities 212 List<TermConcept> collect = theValue.getRootConcepts().stream() 213 .map(val -> myTerminologySvc.findCode(theSystem, val.getCode())) 214 .filter(Optional::isPresent) 215 .map(Optional::get) 216 .collect(Collectors.toList()); 217 218 // Iterate over the actual entities and fill out their children 219 Set<TermConcept> allFoundTermConcepts = collect.stream() 220 .flatMap(concept -> flattenChildren(concept).stream()) 221 .map(suppliedTermConcept -> myTerminologySvc.findCode(theSystem, suppliedTermConcept.getCode())) 222 .filter(Optional::isPresent) 223 .map(Optional::get) 224 .collect(Collectors.toSet()); 225 226 // Delete everything about these codes. 227 for (TermConcept code : allFoundTermConcepts) { 228 deleteEverythingRelatedToConcept(code, removeCounter); 229 } 230 231 return new UploadStatistics(removeCounter.get(), target); 232 } 233 234 private void deleteEverythingRelatedToConcept(TermConcept theConcept, AtomicInteger theRemoveCounter) { 235 236 for (TermConceptParentChildLink nextParent : theConcept.getParents()) { 237 nextParent.getParent().getChildren().remove(nextParent); 238 myConceptParentChildLinkDao.deleteById(nextParent.getId()); 239 } 240 for (TermConceptParentChildLink nextChild : theConcept.getChildren()) { 241 nextChild.getChild().getParents().remove(nextChild); 242 myConceptParentChildLinkDao.deleteById(nextChild.getId()); 243 } 244 245 for (TermConceptDesignation next : theConcept.getDesignations()) { 246 myConceptDesignationDao.deleteById(next.getPid()); 247 } 248 theConcept.getDesignations().clear(); 249 for (TermConceptProperty next : theConcept.getProperties()) { 250 myConceptPropertyDao.deleteById(next.getPid()); 251 } 252 theConcept.getProperties().clear(); 253 254 ourLog.info("Deleting concept {} - Code {}", theConcept.getId(), theConcept.getCode()); 255 256 myConceptDao.deleteById(theConcept.getId()); 257 // myEntityManager.remove(theConcept); 258 259 theRemoveCounter.incrementAndGet(); 260 } 261 262 private List<TermConcept> flattenChildren(TermConcept theTermConcept) { 263 if (theTermConcept.getChildren().isEmpty()) { 264 return Arrays.asList(theTermConcept); 265 } 266 267 // Recursively flatten children 268 List<TermConcept> childTermConcepts = theTermConcept.getChildren().stream() 269 .map(TermConceptParentChildLink::getChild) 270 .flatMap(childConcept -> flattenChildren(childConcept).stream()) 271 .collect(Collectors.toList()); 272 273 // Add itself before its list of children 274 childTermConcepts.add(0, theTermConcept); 275 return childTermConcepts; 276 } 277 278 /** 279 * Returns the number of saved concepts 280 */ 281 @Override 282 public int saveConcept(TermConcept theConcept) { 283 return myTermConceptDaoSvc.saveConcept(theConcept); 284 } 285 286 @Override 287 @Transactional(propagation = Propagation.MANDATORY) 288 public void storeNewCodeSystemVersionIfNeeded( 289 CodeSystem theCodeSystem, ResourceTable theResourceEntity, RequestDetails theRequestDetails) { 290 if (theCodeSystem != null && isNotBlank(theCodeSystem.getUrl())) { 291 String codeSystemUrl = theCodeSystem.getUrl(); 292 if (theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.COMPLETE 293 || theCodeSystem.getContent() == null 294 || theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { 295 ourLog.info( 296 "CodeSystem {} has a status of {}, going to store concepts in terminology tables", 297 theResourceEntity.getIdDt().getValue(), 298 theCodeSystem.getContentElement().getValueAsString()); 299 300 detectDuplicatesInCodeSystem(theCodeSystem); 301 302 Long pid = (Long) theCodeSystem.getUserData(RESOURCE_PID_KEY); 303 assert pid != null; 304 JpaPid codeSystemResourcePid = JpaPid.fromId(pid); 305 306 /* 307 * If this is a not-present codesystem and codesystem version already exists, we don't want to 308 * overwrite the existing version since that will wipe out the existing concepts. We do create 309 * or update the TermCodeSystem table though, since that allows the DB to reject changes that would 310 * result in duplicate CodeSystem.url values. 311 */ 312 if (theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { 313 TermCodeSystem termCodeSystem = myCodeSystemDao.findByCodeSystemUri(theCodeSystem.getUrl()); 314 if (termCodeSystem != null) { 315 TermCodeSystemVersion codeSystemVersion = 316 getExistingTermCodeSystemVersion(termCodeSystem.getPid(), theCodeSystem.getVersion()); 317 if (codeSystemVersion != null) { 318 TermCodeSystem myCodeSystemEntity = getOrCreateDistinctTermCodeSystem( 319 codeSystemResourcePid, 320 theCodeSystem.getUrl(), 321 theCodeSystem.getUrl(), 322 theCodeSystem.getVersion(), 323 theResourceEntity); 324 return; 325 } 326 } 327 } 328 329 TermCodeSystemVersion persCs = new TermCodeSystemVersion(); 330 331 populateCodeSystemVersionProperties(persCs, theCodeSystem, theResourceEntity); 332 333 persCs.getConcepts().addAll(TermReadSvcImpl.toPersistedConcepts(theCodeSystem.getConcept(), persCs)); 334 ourLog.debug("Code system has {} concepts", persCs.getConcepts().size()); 335 storeNewCodeSystemVersion( 336 codeSystemResourcePid, 337 codeSystemUrl, 338 theCodeSystem.getName(), 339 theCodeSystem.getVersion(), 340 persCs, 341 theResourceEntity, 342 theRequestDetails); 343 } 344 } 345 } 346 347 private static void detectDuplicatesInCodeSystem(CodeSystem theCodeSystem) { 348 detectDuplicatesInCodeSystem(theCodeSystem.getConcept(), new HashSet<>()); 349 } 350 351 private static void detectDuplicatesInCodeSystem( 352 List<CodeSystem.ConceptDefinitionComponent> theCodeList, Set<String> theFoundCodesBuffer) { 353 for (var next : theCodeList) { 354 if (isNotBlank(next.getCode())) { 355 if (!theFoundCodesBuffer.add(next.getCode())) { 356 /* 357 * Note: We could possibly modify this behaviour to be forgiving, and just 358 * ignore duplicates. The only issue is that concepts can have properties, 359 * designations, etc. and it could be dangerous to just pick one and ignore the 360 * other. So the safer thing seems to be to just throw an error. 361 */ 362 throw new PreconditionFailedException(Msg.code(2528) + "Duplicate concept detected in CodeSystem: " 363 + UrlUtil.sanitizeUrlPart(next.getCode())); 364 } 365 } 366 // Test child concepts within the parent concept 367 detectDuplicatesInCodeSystem(next.getConcept(), theFoundCodesBuffer); 368 } 369 } 370 371 @Override 372 @Transactional 373 public IIdType storeNewCodeSystemVersion( 374 CodeSystem theCodeSystemResource, 375 TermCodeSystemVersion theCodeSystemVersion, 376 RequestDetails theRequest, 377 List<ValueSet> theValueSets, 378 List<ConceptMap> theConceptMaps) { 379 assert TransactionSynchronizationManager.isActualTransactionActive(); 380 381 Validate.notBlank(theCodeSystemResource.getUrl(), "theCodeSystemResource must have a URL"); 382 383 // Note that this creates the TermCodeSystem and TermCodeSystemVersion entities if needed 384 IIdType csId = myTerminologyVersionAdapterSvc.createOrUpdateCodeSystem(theCodeSystemResource, theRequest); 385 386 JpaPid codeSystemResourcePid = myIdHelperService.resolveResourceIdentityPid( 387 RequestPartitionId.allPartitions(), 388 csId.getResourceType(), 389 csId.getIdPart(), 390 ResolveIdentityMode.includeDeleted().cacheOk()); 391 ResourceTable resource = myResourceTableDao.getOne(codeSystemResourcePid.getId()); 392 393 ourLog.info("CodeSystem resource has ID: {}", csId.getValue()); 394 395 populateCodeSystemVersionProperties(theCodeSystemVersion, theCodeSystemResource, resource); 396 397 storeNewCodeSystemVersion( 398 codeSystemResourcePid, 399 theCodeSystemResource.getUrl(), 400 theCodeSystemResource.getName(), 401 theCodeSystemResource.getVersion(), 402 theCodeSystemVersion, 403 resource, 404 theRequest); 405 406 myDeferredStorageSvc.addConceptMapsToStorageQueue(theConceptMaps); 407 myDeferredStorageSvc.addValueSetsToStorageQueue(theValueSets); 408 409 return csId; 410 } 411 412 @Override 413 @Transactional 414 public void storeNewCodeSystemVersion( 415 IResourcePersistentId theCodeSystemResourcePid, 416 String theSystemUri, 417 String theSystemName, 418 String theCodeSystemVersionId, 419 TermCodeSystemVersion theCodeSystemVersion, 420 ResourceTable theCodeSystemResourceTable, 421 RequestDetails theRequestDetails) { 422 assert TransactionSynchronizationManager.isActualTransactionActive(); 423 424 ourLog.debug("Storing code system"); 425 426 TermCodeSystemVersion codeSystemToStore = theCodeSystemVersion; 427 ValidateUtil.isTrueOrThrowInvalidRequest(codeSystemToStore.getResource() != null, "No resource supplied"); 428 ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystemUri, "No system URI supplied"); 429 430 TermCodeSystem codeSystem = getOrCreateDistinctTermCodeSystem( 431 theCodeSystemResourcePid, 432 theSystemUri, 433 theSystemName, 434 theCodeSystemVersionId, 435 theCodeSystemResourceTable); 436 437 List<TermCodeSystemVersion> existing = 438 myCodeSystemVersionDao.findByCodeSystemResourcePid(((JpaPid) theCodeSystemResourcePid).getId()); 439 for (TermCodeSystemVersion next : existing) { 440 if (Objects.equals(next.getCodeSystemVersionId(), theCodeSystemVersionId) 441 && myConceptDao.countByCodeSystemVersion(next.getPid()) == 0) { 442 443 /* 444 * If we already have a CodeSystemVersion that matches the version we're storing, we 445 * can reuse it. 446 */ 447 next.setCodeSystemDisplayName(theSystemName); 448 codeSystemToStore = next; 449 450 } else { 451 452 /* 453 * If we already have a TermCodeSystemVersion that corresponds to the FHIR Resource ID we're 454 * adding a version to, we will mark it for deletion. For any one resource there can only 455 * be one TermCodeSystemVersion entity in the DB. Multiple versions of a codesystem uses 456 * multiple CodeSystem resources with CodeSystem.version set differently (as opposed to 457 * multiple versions of the same CodeSystem, where CodeSystem.meta.versionId is different) 458 */ 459 next.setCodeSystemVersionId("DELETED_" + UUID.randomUUID().toString()); 460 myCodeSystemVersionDao.saveAndFlush(next); 461 myDeferredStorageSvc.deleteCodeSystemVersion(next); 462 } 463 } 464 465 /* 466 * Do the upload 467 */ 468 469 codeSystemToStore.setCodeSystem(codeSystem); 470 codeSystemToStore.setCodeSystemDisplayName(theSystemName); 471 codeSystemToStore.setCodeSystemVersionId(theCodeSystemVersionId); 472 473 ourLog.debug("Validating all codes in CodeSystem for storage (this can take some time for large sets)"); 474 475 // Validate the code system 476 ArrayList<String> conceptsStack = new ArrayList<>(); 477 IdentityHashMap<TermConcept, Object> allConcepts = new IdentityHashMap<>(); 478 int totalCodeCount = 0; 479 Collection<TermConcept> conceptsToSave = theCodeSystemVersion.getConcepts(); 480 for (TermConcept next : conceptsToSave) { 481 totalCodeCount += validateConceptForStorage(next, codeSystemToStore, conceptsStack, allConcepts); 482 } 483 484 ourLog.debug("Saving version containing {} concepts", totalCodeCount); 485 if (codeSystemToStore.getPid() == null) { 486 codeSystemToStore = myCodeSystemVersionDao.saveAndFlush(codeSystemToStore); 487 } 488 489 boolean isMakeVersionCurrent = ITermCodeSystemStorageSvc.isMakeVersionCurrent(theRequestDetails); 490 if (isMakeVersionCurrent) { 491 codeSystem.setCurrentVersion(codeSystemToStore); 492 if (codeSystem.getPid() == null) { 493 codeSystem = myCodeSystemDao.saveAndFlush(codeSystem); 494 } 495 } 496 497 ourLog.debug("Setting CodeSystemVersion[{}] on {} concepts...", codeSystem.getPid(), totalCodeCount); 498 for (TermConcept next : conceptsToSave) { 499 populateVersion(next, codeSystemToStore); 500 } 501 502 ourLog.debug("Saving {} concepts...", totalCodeCount); 503 IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<>(); 504 for (TermConcept next : conceptsToSave) { 505 persistChildren(next, codeSystemToStore, conceptsStack2, totalCodeCount); 506 } 507 508 ourLog.debug("Done saving concepts, flushing to database"); 509 if (!myDeferredStorageSvc.isStorageQueueEmpty(true)) { 510 ourLog.info("Note that some concept saving has been deferred"); 511 } 512 } 513 514 private TermCodeSystemVersion getExistingTermCodeSystemVersion( 515 Long theCodeSystemVersionPid, String theCodeSystemVersion) { 516 TermCodeSystemVersion existing; 517 if (theCodeSystemVersion == null) { 518 existing = myCodeSystemVersionDao.findByCodeSystemPidVersionIsNull(theCodeSystemVersionPid); 519 } else { 520 existing = 521 myCodeSystemVersionDao.findByCodeSystemPidAndVersion(theCodeSystemVersionPid, theCodeSystemVersion); 522 } 523 524 return existing; 525 } 526 527 private void validateDstu3OrNewer() { 528 Validate.isTrue( 529 myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), 530 "Terminology operations only supported in DSTU3+ mode"); 531 } 532 533 private void addConceptInHierarchy( 534 TermCodeSystemVersion theCsv, 535 Collection<String> theParentCodes, 536 TermConcept theConceptToAdd, 537 UploadStatistics theStatisticsTracker, 538 Map<String, TermConcept> theCodeToConcept, 539 int theSequence) { 540 TermConcept conceptToAdd = theConceptToAdd; 541 List<TermConceptParentChildLink> childrenToAdd = theConceptToAdd.getChildren(); 542 543 String nextCodeToAdd = conceptToAdd.getCode(); 544 String parentDescription = "(root concept)"; 545 546 ourLog.info( 547 "Saving concept {} with parent {}", theStatisticsTracker.getUpdatedConceptCount(), parentDescription); 548 549 Optional<TermConcept> existingCodeOpt = myConceptDao.findByCodeSystemAndCode(theCsv.getPid(), nextCodeToAdd); 550 List<TermConceptParentChildLink> existingParentLinks; 551 if (existingCodeOpt.isPresent()) { 552 TermConcept existingCode = existingCodeOpt.get(); 553 existingCode.setIndexStatus(null); 554 existingCode.setDisplay(conceptToAdd.getDisplay()); 555 conceptToAdd = existingCode; 556 existingParentLinks = conceptToAdd.getParents(); 557 } else { 558 existingParentLinks = Collections.emptyList(); 559 } 560 561 Set<TermConcept> parentConceptsWeShouldLinkTo = new HashSet<>(); 562 for (String nextParentCode : theParentCodes) { 563 564 // Don't add parent links that already exist for the code 565 if (existingParentLinks.stream() 566 .anyMatch(t -> t.getParent().getCode().equals(nextParentCode))) { 567 continue; 568 } 569 570 TermConcept nextParentOpt = theCodeToConcept.get(nextParentCode); 571 if (nextParentOpt == null) { 572 nextParentOpt = myConceptDao 573 .findByCodeSystemAndCode(theCsv.getPid(), nextParentCode) 574 .orElse(null); 575 } 576 if (nextParentOpt == null) { 577 throw new InvalidRequestException(Msg.code(846) + "Unable to add code \"" + nextCodeToAdd 578 + "\" to unknown parent: " + nextParentCode); 579 } 580 parentConceptsWeShouldLinkTo.add(nextParentOpt); 581 } 582 583 if (conceptToAdd.getSequence() == null) { 584 conceptToAdd.setSequence(theSequence); 585 } 586 587 // Null out the hierarchy PIDs for this concept always. We do this because we're going to 588 // force a reindex, and it'll be regenerated then 589 conceptToAdd.setParentPids(null); 590 conceptToAdd.setCodeSystemVersion(theCsv); 591 592 if (conceptToAdd.getProperties() != null) 593 conceptToAdd.getProperties().forEach(termConceptProperty -> { 594 termConceptProperty.setConcept(theConceptToAdd); 595 termConceptProperty.setCodeSystemVersion(theCsv); 596 }); 597 if (theStatisticsTracker.getUpdatedConceptCount() <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) { 598 saveConcept(conceptToAdd); 599 Long nextConceptPid = conceptToAdd.getId(); 600 Objects.requireNonNull(nextConceptPid); 601 } else { 602 myDeferredStorageSvc.addConceptToStorageQueue(conceptToAdd); 603 } 604 605 theCodeToConcept.put(conceptToAdd.getCode(), conceptToAdd); 606 607 theStatisticsTracker.incrementUpdatedConceptCount(); 608 609 // Add link to new child to the parent 610 for (TermConcept nextParentConcept : parentConceptsWeShouldLinkTo) { 611 TermConceptParentChildLink parentLink = new TermConceptParentChildLink(); 612 parentLink.setParent(nextParentConcept); 613 parentLink.setChild(conceptToAdd); 614 parentLink.setCodeSystem(theCsv); 615 parentLink.setRelationshipType(TermConceptParentChildLink.RelationshipTypeEnum.ISA); 616 nextParentConcept.getChildren().add(parentLink); 617 conceptToAdd.getParents().add(parentLink); 618 ourLog.info( 619 "Saving parent/child link - Parent[{}] Child[{}]", 620 parentLink.getParent().getCode(), 621 parentLink.getChild().getCode()); 622 623 if (theStatisticsTracker.getUpdatedConceptCount() 624 <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) { 625 myConceptParentChildLinkDao.save(parentLink); 626 } else { 627 myDeferredStorageSvc.addConceptLinkToStorageQueue(parentLink); 628 } 629 } 630 631 ourLog.trace("About to save parent-child links"); 632 633 // Save children recursively 634 int childIndex = 0; 635 for (TermConceptParentChildLink nextChildConceptLink : new ArrayList<>(childrenToAdd)) { 636 637 TermConcept nextChild = nextChildConceptLink.getChild(); 638 639 for (int i = 0; i < nextChild.getParents().size(); i++) { 640 if (nextChild.getParents().get(i).getId() == null) { 641 String parentCode = 642 nextChild.getParents().get(i).getParent().getCode(); 643 TermConcept parentConcept = theCodeToConcept.get(parentCode); 644 if (parentConcept == null) { 645 parentConcept = myConceptDao 646 .findByCodeSystemAndCode(theCsv.getPid(), parentCode) 647 .orElse(null); 648 } 649 if (parentConcept == null) { 650 throw new IllegalArgumentException(Msg.code(847) + "Unknown parent code: " + parentCode); 651 } 652 653 nextChild.getParents().get(i).setParent(parentConcept); 654 } 655 } 656 657 Collection<String> parentCodes = nextChild.getParents().stream() 658 .map(t -> t.getParent().getCode()) 659 .collect(Collectors.toList()); 660 addConceptInHierarchy(theCsv, parentCodes, nextChild, theStatisticsTracker, theCodeToConcept, childIndex); 661 662 childIndex++; 663 } 664 } 665 666 private void persistChildren( 667 TermConcept theConcept, 668 TermCodeSystemVersion theCodeSystem, 669 IdentityHashMap<TermConcept, Object> theConceptsStack, 670 int theTotalConcepts) { 671 if (theConceptsStack.put(theConcept, PLACEHOLDER_OBJECT) != null) { 672 return; 673 } 674 675 if ((theConceptsStack.size() + 1) % 10000 == 0) { 676 float pct = (float) theConceptsStack.size() / (float) theTotalConcepts; 677 ourLog.info( 678 "Have processed {}/{} concepts ({}%)", 679 theConceptsStack.size(), theTotalConcepts, (int) (pct * 100.0f)); 680 } 681 682 theConcept.setCodeSystemVersion(theCodeSystem); 683 theConcept.setIndexStatus(BaseHapiFhirDao.INDEX_STATUS_INDEXED); 684 685 if (theConceptsStack.size() <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) { 686 saveConcept(theConcept); 687 } else { 688 myDeferredStorageSvc.addConceptToStorageQueue(theConcept); 689 } 690 691 for (TermConceptParentChildLink next : theConcept.getChildren()) { 692 persistChildren(next.getChild(), theCodeSystem, theConceptsStack, theTotalConcepts); 693 } 694 695 for (TermConceptParentChildLink next : theConcept.getChildren()) { 696 if (theConceptsStack.size() <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) { 697 saveConceptLink(next); 698 } else { 699 myDeferredStorageSvc.addConceptLinkToStorageQueue(next); 700 } 701 } 702 } 703 704 private void populateVersion(TermConcept theNext, TermCodeSystemVersion theCodeSystemVersion) { 705 theNext.setCodeSystemVersion(theCodeSystemVersion); 706 for (TermConceptParentChildLink next : theNext.getChildren()) { 707 populateVersion(next.getChild(), theCodeSystemVersion); 708 } 709 theNext.getProperties().forEach(t -> t.setCodeSystemVersion(theCodeSystemVersion)); 710 theNext.getDesignations().forEach(t -> t.setCodeSystemVersion(theCodeSystemVersion)); 711 } 712 713 private void saveConceptLink(TermConceptParentChildLink next) { 714 if (next.getId() == null) { 715 myConceptParentChildLinkDao.save(next); 716 } 717 } 718 719 @Nonnull 720 private TermCodeSystem getOrCreateDistinctTermCodeSystem( 721 IResourcePersistentId theCodeSystemResourcePid, 722 String theSystemUri, 723 String theSystemName, 724 String theSystemVersionId, 725 ResourceTable theCodeSystemResourceTable) { 726 TermCodeSystem codeSystem = myCodeSystemDao.findByCodeSystemUri(theSystemUri); 727 if (codeSystem == null) { 728 codeSystem = myCodeSystemDao.findByResourcePid(((JpaPid) theCodeSystemResourcePid).getId()); 729 if (codeSystem == null) { 730 codeSystem = new TermCodeSystem(); 731 } 732 } else { 733 checkForCodeSystemVersionDuplicate( 734 codeSystem, theSystemUri, theSystemVersionId, theCodeSystemResourceTable); 735 } 736 737 codeSystem.setResource(theCodeSystemResourceTable); 738 codeSystem.setCodeSystemUri(theSystemUri); 739 codeSystem.setName(theSystemName); 740 codeSystem = myCodeSystemDao.save(codeSystem); 741 return codeSystem; 742 } 743 744 private void checkForCodeSystemVersionDuplicate( 745 TermCodeSystem theCodeSystem, 746 String theSystemUri, 747 String theSystemVersionId, 748 ResourceTable theCodeSystemResourceTable) { 749 TermCodeSystemVersion codeSystemVersionEntity; 750 String msg = null; 751 if (theSystemVersionId == null) { 752 // Check if a non-versioned TermCodeSystemVersion entity already exists for this TermCodeSystem. 753 codeSystemVersionEntity = myCodeSystemVersionDao.findByCodeSystemPidVersionIsNull(theCodeSystem.getPid()); 754 if (codeSystemVersionEntity != null) { 755 msg = myContext 756 .getLocalizer() 757 .getMessage( 758 TermReadSvcImpl.class, 759 "cannotCreateDuplicateCodeSystemUrl", 760 theSystemUri, 761 codeSystemVersionEntity 762 .getResource() 763 .getIdDt() 764 .toUnqualifiedVersionless() 765 .getValue()); 766 } 767 } else { 768 // Check if a TermCodeSystemVersion entity already exists for this TermCodeSystem and version. 769 codeSystemVersionEntity = 770 myCodeSystemVersionDao.findByCodeSystemPidAndVersion(theCodeSystem.getPid(), theSystemVersionId); 771 if (codeSystemVersionEntity != null) { 772 msg = myContext 773 .getLocalizer() 774 .getMessage( 775 TermReadSvcImpl.class, 776 "cannotCreateDuplicateCodeSystemUrlAndVersion", 777 theSystemUri, 778 theSystemVersionId, 779 codeSystemVersionEntity 780 .getResource() 781 .getIdDt() 782 .toUnqualifiedVersionless() 783 .getValue()); 784 } 785 } 786 // Throw exception if the TermCodeSystemVersion is being duplicated. 787 if (codeSystemVersionEntity != null) { 788 if (!ObjectUtil.equals(codeSystemVersionEntity.getResource().getId(), theCodeSystemResourceTable.getId())) { 789 throw new UnprocessableEntityException(Msg.code(848) + msg); 790 } 791 } 792 } 793 794 private void populateCodeSystemVersionProperties( 795 TermCodeSystemVersion theCodeSystemVersion, 796 CodeSystem theCodeSystemResource, 797 ResourceTable theResourceTable) { 798 theCodeSystemVersion.setResource(theResourceTable); 799 theCodeSystemVersion.setCodeSystemDisplayName(theCodeSystemResource.getName()); 800 theCodeSystemVersion.setCodeSystemVersionId(theCodeSystemResource.getVersion()); 801 } 802 803 private int validateConceptForStorage( 804 TermConcept theConcept, 805 TermCodeSystemVersion theCodeSystemVersion, 806 ArrayList<String> theConceptsStack, 807 IdentityHashMap<TermConcept, Object> theAllConcepts) { 808 ValidateUtil.isTrueOrThrowInvalidRequest( 809 theConcept.getCodeSystemVersion() != null, "CodeSystemVersion is null"); 810 ValidateUtil.isNotBlankOrThrowInvalidRequest( 811 theConcept.getCode(), "CodeSystem contains a code with no code value"); 812 813 theConcept.setCodeSystemVersion(theCodeSystemVersion); 814 if (theConceptsStack.contains(theConcept.getCode())) { 815 throw new InvalidRequestException( 816 Msg.code(849) + "CodeSystem contains circular reference around code " + theConcept.getCode()); 817 } 818 theConceptsStack.add(theConcept.getCode()); 819 820 int retVal = 0; 821 if (theAllConcepts.put(theConcept, theAllConcepts) == null) { 822 if (theAllConcepts.size() % 1000 == 0) { 823 ourLog.info("Have validated {} concepts", theAllConcepts.size()); 824 } 825 retVal = 1; 826 } 827 828 for (TermConceptParentChildLink next : theConcept.getChildren()) { 829 next.setCodeSystem(theCodeSystemVersion); 830 retVal += 831 validateConceptForStorage(next.getChild(), theCodeSystemVersion, theConceptsStack, theAllConcepts); 832 } 833 834 theConceptsStack.remove(theConceptsStack.size() - 1); 835 836 return retVal; 837 } 838}