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