001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2023 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.support.TranslateConceptResult; 024import ca.uhn.fhir.context.support.TranslateConceptResults; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.model.TranslationQuery; 028import ca.uhn.fhir.jpa.api.model.TranslationRequest; 029import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 030import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao; 031import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao; 032import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao; 033import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao; 034import ca.uhn.fhir.jpa.entity.TermConceptMap; 035import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; 036import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; 037import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; 038import ca.uhn.fhir.jpa.model.dao.JpaPid; 039import ca.uhn.fhir.jpa.model.entity.ResourceTable; 040import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; 041import ca.uhn.fhir.jpa.util.MemoryCacheService; 042import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; 043import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 044import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 045import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 046import ca.uhn.fhir.util.ValidateUtil; 047import com.google.common.annotations.VisibleForTesting; 048import org.apache.commons.lang3.StringUtils; 049import org.hibernate.ScrollMode; 050import org.hibernate.ScrollableResults; 051import org.hl7.fhir.exceptions.FHIRException; 052import org.hl7.fhir.instance.model.api.IIdType; 053import org.hl7.fhir.r4.model.BooleanType; 054import org.hl7.fhir.r4.model.CodeType; 055import org.hl7.fhir.r4.model.Coding; 056import org.hl7.fhir.r4.model.ConceptMap; 057import org.hl7.fhir.r4.model.IdType; 058import org.hl7.fhir.r4.model.Parameters; 059import org.hl7.fhir.r4.model.StringType; 060import org.hl7.fhir.r4.model.UriType; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063import org.springframework.beans.factory.annotation.Autowired; 064import org.springframework.data.domain.PageRequest; 065import org.springframework.data.domain.Pageable; 066import org.springframework.transaction.annotation.Propagation; 067import org.springframework.transaction.annotation.Transactional; 068 069import java.util.ArrayList; 070import java.util.HashSet; 071import java.util.List; 072import java.util.Optional; 073import java.util.Set; 074import javax.persistence.EntityManager; 075import javax.persistence.PersistenceContext; 076import javax.persistence.PersistenceContextType; 077import javax.persistence.TypedQuery; 078import javax.persistence.criteria.CriteriaBuilder; 079import javax.persistence.criteria.CriteriaQuery; 080import javax.persistence.criteria.Join; 081import javax.persistence.criteria.Predicate; 082import javax.persistence.criteria.Root; 083 084import static ca.uhn.fhir.jpa.term.TermReadSvcImpl.isPlaceholder; 085import static org.apache.commons.lang3.StringUtils.isBlank; 086import static org.apache.commons.lang3.StringUtils.isNotBlank; 087 088public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc { 089 090 private static final Logger ourLog = LoggerFactory.getLogger(TermConceptMappingSvcImpl.class); 091 private static boolean ourLastResultsFromTranslationCache; // For testing. 092 private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing. 093 private final int myFetchSize = TermReadSvcImpl.DEFAULT_FETCH_SIZE; 094 095 @Autowired 096 protected ITermConceptMapDao myConceptMapDao; 097 098 @Autowired 099 protected ITermConceptMapGroupDao myConceptMapGroupDao; 100 101 @Autowired 102 protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao; 103 104 @Autowired 105 protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao; 106 107 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 108 protected EntityManager myEntityManager; 109 110 @Autowired 111 private FhirContext myContext; 112 113 @Autowired 114 private MemoryCacheService myMemoryCacheService; 115 116 @Autowired 117 private IIdHelperService<JpaPid> myIdHelperService; 118 119 @Override 120 @Transactional 121 public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { 122 deleteConceptMap(theResourceTable); 123 } 124 125 @Override 126 public FhirContext getFhirContext() { 127 return myContext; 128 } 129 130 @Override 131 @Transactional 132 public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { 133 TranslationRequest request = TranslationRequest.fromTranslateCodeRequest(theRequest); 134 if (request.hasReverse() && request.getReverseAsBoolean()) { 135 return translateWithReverse(request); 136 } 137 138 return translate(request); 139 } 140 141 @Override 142 @Transactional 143 public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) { 144 145 ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied"); 146 if (isPlaceholder(theConceptMap)) { 147 ourLog.info( 148 "Not storing TermConceptMap for placeholder {}", 149 theConceptMap.getIdElement().toVersionless().getValueAsString()); 150 return; 151 } 152 153 ValidateUtil.isNotBlankOrThrowUnprocessableEntity( 154 theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url"); 155 ourLog.info( 156 "Storing TermConceptMap for {}", 157 theConceptMap.getIdElement().toVersionless().getValueAsString()); 158 159 TermConceptMap termConceptMap = new TermConceptMap(); 160 termConceptMap.setResource(theResourceTable); 161 termConceptMap.setUrl(theConceptMap.getUrl()); 162 termConceptMap.setVersion(theConceptMap.getVersion()); 163 164 String source = theConceptMap.hasSourceUriType() 165 ? theConceptMap.getSourceUriType().getValueAsString() 166 : null; 167 String target = theConceptMap.hasTargetUriType() 168 ? theConceptMap.getTargetUriType().getValueAsString() 169 : null; 170 171 /* 172 * If this is a mapping between "resources" instead of purely between 173 * "concepts" (this is a weird concept that is technically possible, at least as of 174 * FHIR R4), don't try to store the mappings. 175 * 176 * See here for a description of what that is: 177 * http://hl7.org/fhir/conceptmap.html#bnr 178 */ 179 if ("StructureDefinition".equals(new IdType(source).getResourceType()) 180 || "StructureDefinition".equals(new IdType(target).getResourceType())) { 181 return; 182 } 183 184 if (source == null && theConceptMap.hasSourceCanonicalType()) { 185 source = theConceptMap.getSourceCanonicalType().getValueAsString(); 186 } 187 if (target == null && theConceptMap.hasTargetCanonicalType()) { 188 target = theConceptMap.getTargetCanonicalType().getValueAsString(); 189 } 190 191 /* 192 * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions. 193 */ 194 deleteConceptMap(theResourceTable); 195 196 /* 197 * Do the upload. 198 */ 199 String conceptMapUrl = termConceptMap.getUrl(); 200 String conceptMapVersion = termConceptMap.getVersion(); 201 Optional<TermConceptMap> optionalExistingTermConceptMapByUrl; 202 if (isBlank(conceptMapVersion)) { 203 optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl); 204 } else { 205 optionalExistingTermConceptMapByUrl = 206 myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion); 207 } 208 if (!optionalExistingTermConceptMapByUrl.isPresent()) { 209 try { 210 if (isNotBlank(source)) { 211 termConceptMap.setSource(source); 212 } 213 if (isNotBlank(target)) { 214 termConceptMap.setTarget(target); 215 } 216 } catch (FHIRException fe) { 217 throw new InternalErrorException(Msg.code(837) + fe); 218 } 219 termConceptMap = myConceptMapDao.save(termConceptMap); 220 int codesSaved = 0; 221 222 TermConceptMapGroup termConceptMapGroup; 223 for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) { 224 225 String groupSource = group.getSource(); 226 if (isBlank(groupSource)) { 227 groupSource = source; 228 } 229 if (isBlank(groupSource)) { 230 throw new UnprocessableEntityException(Msg.code(838) + "ConceptMap[url='" + theConceptMap.getUrl() 231 + "'] contains at least one group without a value in ConceptMap.group.source"); 232 } 233 234 String groupTarget = group.getTarget(); 235 if (isBlank(groupTarget)) { 236 groupTarget = target; 237 } 238 if (isBlank(groupTarget)) { 239 throw new UnprocessableEntityException(Msg.code(839) + "ConceptMap[url='" + theConceptMap.getUrl() 240 + "'] contains at least one group without a value in ConceptMap.group.target"); 241 } 242 243 termConceptMapGroup = new TermConceptMapGroup(); 244 termConceptMapGroup.setConceptMap(termConceptMap); 245 termConceptMapGroup.setSource(groupSource); 246 termConceptMapGroup.setSourceVersion(group.getSourceVersion()); 247 termConceptMapGroup.setTarget(groupTarget); 248 termConceptMapGroup.setTargetVersion(group.getTargetVersion()); 249 termConceptMap.getConceptMapGroups().add(termConceptMapGroup); 250 termConceptMapGroup = myConceptMapGroupDao.save(termConceptMapGroup); 251 252 if (group.hasElement()) { 253 TermConceptMapGroupElement termConceptMapGroupElement; 254 for (ConceptMap.SourceElementComponent element : group.getElement()) { 255 if (isBlank(element.getCode())) { 256 continue; 257 } 258 termConceptMapGroupElement = new TermConceptMapGroupElement(); 259 termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup); 260 termConceptMapGroupElement.setCode(element.getCode()); 261 termConceptMapGroupElement.setDisplay(element.getDisplay()); 262 termConceptMapGroup.getConceptMapGroupElements().add(termConceptMapGroupElement); 263 termConceptMapGroupElement = myConceptMapGroupElementDao.save(termConceptMapGroupElement); 264 265 if (element.hasTarget()) { 266 TermConceptMapGroupElementTarget termConceptMapGroupElementTarget; 267 for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) { 268 if (isBlank(elementTarget.getCode())) { 269 continue; 270 } 271 termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget(); 272 termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement); 273 termConceptMapGroupElementTarget.setCode(elementTarget.getCode()); 274 termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay()); 275 termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence()); 276 termConceptMapGroupElement 277 .getConceptMapGroupElementTargets() 278 .add(termConceptMapGroupElementTarget); 279 myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget); 280 281 if (++codesSaved % 250 == 0) { 282 ourLog.info("Have saved {} codes in ConceptMap", codesSaved); 283 myConceptMapGroupElementTargetDao.flush(); 284 } 285 } 286 } 287 } 288 } 289 } 290 291 } else { 292 TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get(); 293 294 if (isBlank(conceptMapVersion)) { 295 String msg = myContext 296 .getLocalizer() 297 .getMessage( 298 TermReadSvcImpl.class, 299 "cannotCreateDuplicateConceptMapUrl", 300 conceptMapUrl, 301 existingTermConceptMap 302 .getResource() 303 .getIdDt() 304 .toUnqualifiedVersionless() 305 .getValue()); 306 throw new UnprocessableEntityException(Msg.code(840) + msg); 307 308 } else { 309 String msg = myContext 310 .getLocalizer() 311 .getMessage( 312 TermReadSvcImpl.class, 313 "cannotCreateDuplicateConceptMapUrlAndVersion", 314 conceptMapUrl, 315 conceptMapVersion, 316 existingTermConceptMap 317 .getResource() 318 .getIdDt() 319 .toUnqualifiedVersionless() 320 .getValue()); 321 throw new UnprocessableEntityException(Msg.code(841) + msg); 322 } 323 } 324 325 ourLog.info( 326 "Done storing TermConceptMap[{}] for {}", 327 termConceptMap.getId(), 328 theConceptMap.getIdElement().toVersionless().getValueAsString()); 329 } 330 331 @Override 332 @Transactional(propagation = Propagation.REQUIRED) 333 public TranslateConceptResults translate(TranslationRequest theTranslationRequest) { 334 TranslateConceptResults retVal = new TranslateConceptResults(); 335 336 CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder(); 337 CriteriaQuery<TermConceptMapGroupElementTarget> query = 338 criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class); 339 Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class); 340 341 Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin = 342 root.join("myConceptMapGroupElement"); 343 Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup"); 344 Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap"); 345 346 List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries(); 347 List<TranslateConceptResult> cachedTargets; 348 ArrayList<Predicate> predicates; 349 Coding coding; 350 351 // -- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version 352 String latestConceptMapVersion = null; 353 if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion()) 354 latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest); 355 356 for (TranslationQuery translationQuery : translationQueries) { 357 cachedTargets = myMemoryCacheService.getIfPresent( 358 MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery); 359 if (cachedTargets == null) { 360 final List<TranslateConceptResult> targets = new ArrayList<>(); 361 362 predicates = new ArrayList<>(); 363 364 coding = translationQuery.getCoding(); 365 if (coding.hasCode()) { 366 predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode())); 367 } else { 368 throw new InvalidRequestException( 369 Msg.code(842) + "A code must be provided for translation to occur."); 370 } 371 372 if (coding.hasSystem()) { 373 predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem())); 374 } 375 376 if (coding.hasVersion()) { 377 predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion())); 378 } 379 380 if (translationQuery.hasTargetSystem()) { 381 predicates.add( 382 criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem())); 383 } 384 385 if (translationQuery.hasUrl()) { 386 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl())); 387 if (translationQuery.hasConceptMapVersion()) { 388 // both url and conceptMapVersion 389 predicates.add(criteriaBuilder.equal( 390 conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion())); 391 } else { 392 if (StringUtils.isNotBlank(latestConceptMapVersion)) { 393 // only url and use latestConceptMapVersion 394 predicates.add( 395 criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion)); 396 } else { 397 predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion"))); 398 } 399 } 400 } 401 402 if (translationQuery.hasSource()) { 403 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource())); 404 } 405 406 if (translationQuery.hasTarget()) { 407 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget())); 408 } 409 410 if (translationQuery.hasResourceId()) { 411 IIdType resourceId = translationQuery.getResourceId(); 412 JpaPid resourcePid = 413 myIdHelperService.getPidOrThrowException(RequestPartitionId.defaultPartition(), resourceId); 414 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), resourcePid.getId())); 415 } 416 417 Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0])); 418 query.where(outerPredicate); 419 420 // Use scrollable results. 421 final TypedQuery<TermConceptMapGroupElementTarget> typedQuery = 422 myEntityManager.createQuery(query.select(root)); 423 org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery = 424 (org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery; 425 hibernateQuery.setFetchSize(myFetchSize); 426 ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); 427 try (ScrollableResultsIterator<TermConceptMapGroupElementTarget> scrollableResultsIterator = 428 new ScrollableResultsIterator<>(scrollableResults)) { 429 430 Set<TermConceptMapGroupElementTarget> matches = new HashSet<>(); 431 while (scrollableResultsIterator.hasNext()) { 432 TermConceptMapGroupElementTarget next = scrollableResultsIterator.next(); 433 if (matches.add(next)) { 434 435 TranslateConceptResult translationMatch = new TranslateConceptResult(); 436 if (next.getEquivalence() != null) { 437 translationMatch.setEquivalence( 438 next.getEquivalence().toCode()); 439 } 440 441 translationMatch.setCode(next.getCode()); 442 translationMatch.setSystem(next.getSystem()); 443 translationMatch.setSystemVersion(next.getSystemVersion()); 444 translationMatch.setDisplay(next.getDisplay()); 445 translationMatch.setValueSet(next.getValueSet()); 446 translationMatch.setSystemVersion(next.getSystemVersion()); 447 translationMatch.setConceptMapUrl(next.getConceptMapUrl()); 448 449 targets.add(translationMatch); 450 } 451 } 452 } 453 454 ourLastResultsFromTranslationCache = false; // For testing. 455 myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery, targets); 456 retVal.getResults().addAll(targets); 457 } else { 458 ourLastResultsFromTranslationCache = true; // For testing. 459 retVal.getResults().addAll(cachedTargets); 460 } 461 } 462 463 buildTranslationResult(retVal); 464 return retVal; 465 } 466 467 @Override 468 @Transactional(propagation = Propagation.REQUIRED) 469 public TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest) { 470 TranslateConceptResults retVal = new TranslateConceptResults(); 471 472 CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder(); 473 CriteriaQuery<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class); 474 Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class); 475 476 Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin = 477 root.join("myConceptMapGroupElementTargets"); 478 Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup"); 479 Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap"); 480 481 List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries(); 482 List<TranslateConceptResult> cachedElements; 483 ArrayList<Predicate> predicates; 484 Coding coding; 485 486 // -- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version 487 String latestConceptMapVersion = null; 488 if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion()) 489 latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest); 490 491 for (TranslationQuery translationQuery : translationQueries) { 492 cachedElements = myMemoryCacheService.getIfPresent( 493 MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery); 494 if (cachedElements == null) { 495 final List<TranslateConceptResult> elements = new ArrayList<>(); 496 497 predicates = new ArrayList<>(); 498 499 coding = translationQuery.getCoding(); 500 String targetCode; 501 String targetCodeSystem = null; 502 if (coding.hasCode()) { 503 predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode())); 504 targetCode = coding.getCode(); 505 } else { 506 throw new InvalidRequestException( 507 Msg.code(843) + "A code must be provided for translation to occur."); 508 } 509 510 if (coding.hasSystem()) { 511 predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem())); 512 targetCodeSystem = coding.getSystem(); 513 } 514 515 if (coding.hasVersion()) { 516 predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion())); 517 } 518 519 if (translationQuery.hasUrl()) { 520 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl())); 521 if (translationQuery.hasConceptMapVersion()) { 522 // both url and conceptMapVersion 523 predicates.add(criteriaBuilder.equal( 524 conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion())); 525 } else { 526 if (StringUtils.isNotBlank(latestConceptMapVersion)) { 527 // only url and use latestConceptMapVersion 528 predicates.add( 529 criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion)); 530 } else { 531 predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion"))); 532 } 533 } 534 } 535 536 if (translationQuery.hasTargetSystem()) { 537 predicates.add( 538 criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem())); 539 } 540 541 if (translationQuery.hasSource()) { 542 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource())); 543 } 544 545 if (translationQuery.hasTarget()) { 546 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget())); 547 } 548 549 if (translationQuery.hasResourceId()) { 550 IIdType resourceId = translationQuery.getResourceId(); 551 JpaPid resourcePid = 552 myIdHelperService.getPidOrThrowException(RequestPartitionId.defaultPartition(), resourceId); 553 predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), resourcePid.getId())); 554 } 555 556 Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0])); 557 query.where(outerPredicate); 558 559 // Use scrollable results. 560 final TypedQuery<TermConceptMapGroupElement> typedQuery = 561 myEntityManager.createQuery(query.select(root)); 562 org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery = 563 (org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery; 564 hibernateQuery.setFetchSize(myFetchSize); 565 ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); 566 try (ScrollableResultsIterator<TermConceptMapGroupElement> scrollableResultsIterator = 567 new ScrollableResultsIterator<>(scrollableResults)) { 568 569 Set<TermConceptMapGroupElementTarget> matches = new HashSet<>(); 570 while (scrollableResultsIterator.hasNext()) { 571 TermConceptMapGroupElement nextElement = scrollableResultsIterator.next(); 572 573 /* TODO: The invocation of the size() below does not seem to be necessary but for some reason, 574 * but removing it causes tests in TerminologySvcImplR4Test to fail. We use the outcome 575 * in a trace log to avoid ErrorProne flagging an unused return value. 576 */ 577 int size = 578 nextElement.getConceptMapGroupElementTargets().size(); 579 ourLog.trace("Have {} targets", size); 580 581 myEntityManager.detach(nextElement); 582 583 if (isNotBlank(targetCode)) { 584 for (TermConceptMapGroupElementTarget next : 585 nextElement.getConceptMapGroupElementTargets()) { 586 if (matches.add(next)) { 587 if (isBlank(targetCodeSystem) 588 || StringUtils.equals(targetCodeSystem, next.getSystem())) { 589 if (StringUtils.equals(targetCode, next.getCode())) { 590 TranslateConceptResult translationMatch = new TranslateConceptResult(); 591 translationMatch.setCode(nextElement.getCode()); 592 translationMatch.setSystem(nextElement.getSystem()); 593 translationMatch.setSystemVersion(nextElement.getSystemVersion()); 594 translationMatch.setDisplay(nextElement.getDisplay()); 595 translationMatch.setValueSet(nextElement.getValueSet()); 596 translationMatch.setSystemVersion(nextElement.getSystemVersion()); 597 translationMatch.setConceptMapUrl(nextElement.getConceptMapUrl()); 598 if (next.getEquivalence() != null) { 599 translationMatch.setEquivalence( 600 next.getEquivalence().toCode()); 601 } 602 603 if (alreadyContainsMapping(elements, translationMatch) 604 || alreadyContainsMapping(retVal.getResults(), translationMatch)) { 605 continue; 606 } 607 608 elements.add(translationMatch); 609 } 610 } 611 } 612 } 613 } 614 } 615 } 616 617 ourLastResultsFromTranslationWithReverseCache = false; // For testing. 618 myMemoryCacheService.put( 619 MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery, elements); 620 retVal.getResults().addAll(elements); 621 } else { 622 ourLastResultsFromTranslationWithReverseCache = true; // For testing. 623 retVal.getResults().addAll(cachedElements); 624 } 625 } 626 627 buildTranslationResult(retVal); 628 return retVal; 629 } 630 631 private boolean alreadyContainsMapping( 632 List<TranslateConceptResult> elements, TranslateConceptResult translationMatch) { 633 for (TranslateConceptResult nextExistingElement : elements) { 634 if (StringUtils.equals(nextExistingElement.getSystem(), translationMatch.getSystem())) { 635 if (StringUtils.equals(nextExistingElement.getSystemVersion(), translationMatch.getSystemVersion())) { 636 if (StringUtils.equals(nextExistingElement.getCode(), translationMatch.getCode())) { 637 return true; 638 } 639 } 640 } 641 } 642 return false; 643 } 644 645 public void deleteConceptMap(ResourceTable theResourceTable) { 646 // Get existing entity so it can be deleted. 647 Optional<TermConceptMap> optionalExistingTermConceptMapById = 648 myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId()); 649 650 if (optionalExistingTermConceptMapById.isPresent()) { 651 TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get(); 652 653 ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId()); 654 for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) { 655 656 for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) { 657 658 for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) { 659 660 myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId()); 661 } 662 663 myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId()); 664 } 665 666 myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId()); 667 } 668 669 myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId()); 670 ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId()); 671 } 672 } 673 674 // Special case for the translate operation with url and without 675 // conceptMapVersion, find the latest conecptMapVersion 676 private String getLatestConceptMapVersion(TranslationRequest theTranslationRequest) { 677 678 Pageable page = PageRequest.of(0, 1); 679 List<TermConceptMap> theConceptMapList = myConceptMapDao.getTermConceptMapEntitiesByUrlOrderByMostRecentUpdate( 680 page, theTranslationRequest.getUrl()); 681 if (!theConceptMapList.isEmpty()) { 682 return theConceptMapList.get(0).getVersion(); 683 } 684 685 return null; 686 } 687 688 private void buildTranslationResult(TranslateConceptResults theTranslationResult) { 689 690 String msg; 691 if (theTranslationResult.getResults().isEmpty()) { 692 theTranslationResult.setResult(false); 693 msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "noMatchesFound"); 694 theTranslationResult.setMessage(msg); 695 } else { 696 theTranslationResult.setResult(true); 697 msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "matchesFound"); 698 theTranslationResult.setMessage(msg); 699 } 700 } 701 702 /** 703 * This method is present only for unit tests, do not call from client code 704 */ 705 @VisibleForTesting 706 public static void clearOurLastResultsFromTranslationCache() { 707 ourLastResultsFromTranslationCache = false; 708 } 709 710 /** 711 * This method is present only for unit tests, do not call from client code 712 */ 713 @VisibleForTesting 714 public static void clearOurLastResultsFromTranslationWithReverseCache() { 715 ourLastResultsFromTranslationWithReverseCache = false; 716 } 717 718 /** 719 * This method is present only for unit tests, do not call from client code 720 */ 721 @VisibleForTesting 722 static boolean isOurLastResultsFromTranslationCache() { 723 return ourLastResultsFromTranslationCache; 724 } 725 726 /** 727 * This method is present only for unit tests, do not call from client code 728 */ 729 @VisibleForTesting 730 static boolean isOurLastResultsFromTranslationWithReverseCache() { 731 return ourLastResultsFromTranslationWithReverseCache; 732 } 733 734 public static Parameters toParameters(TranslateConceptResults theTranslationResult) { 735 Parameters retVal = new Parameters(); 736 737 retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult())); 738 739 if (theTranslationResult.getMessage() != null) { 740 retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage())); 741 } 742 743 for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) { 744 Parameters.ParametersParameterComponent matchParam = 745 retVal.addParameter().setName("match"); 746 populateTranslateMatchParts(translationMatch, matchParam); 747 } 748 749 return retVal; 750 } 751 752 private static void populateTranslateMatchParts( 753 TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) { 754 if (theTranslationMatch.getEquivalence() != null) { 755 theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence())); 756 } 757 758 if (isNotBlank(theTranslationMatch.getSystem()) 759 || isNotBlank(theTranslationMatch.getCode()) 760 || isNotBlank(theTranslationMatch.getDisplay())) { 761 Coding value = new Coding( 762 theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay()); 763 764 if (isNotBlank(theTranslationMatch.getSystemVersion())) { 765 value.setVersion(theTranslationMatch.getSystemVersion()); 766 } 767 768 theParam.addPart().setName("concept").setValue(value); 769 } 770 771 if (isNotBlank(theTranslationMatch.getConceptMapUrl())) { 772 theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl())); 773 } 774 } 775}