diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptClientMappingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptClientMappingSvcImpl.java new file mode 100644 index 00000000000..afee40feeab --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptClientMappingSvcImpl.java @@ -0,0 +1,420 @@ +package ca.uhn.fhir.jpa.term; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.TranslateConceptResult; +import ca.uhn.fhir.context.support.TranslateConceptResults; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.model.TranslationQuery; +import ca.uhn.fhir.jpa.api.model.TranslationRequest; +import ca.uhn.fhir.jpa.api.svc.IIdHelperService; +import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao; +import ca.uhn.fhir.jpa.entity.TermConceptMap; +import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; +import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; +import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; +import ca.uhn.fhir.jpa.model.dao.JpaPid; +import ca.uhn.fhir.jpa.term.api.ITermConceptClientMappingSvc; +import ca.uhn.fhir.jpa.util.MemoryCacheService; +import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.ScrollMode; +import org.hibernate.ScrollableResults; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Coding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class TermConceptClientMappingSvcImpl implements ITermConceptClientMappingSvc { + private static final Logger ourLog = LoggerFactory.getLogger(TermConceptClientMappingSvcImpl.class); + + private final int myFetchSize = TermReadSvcImpl.DEFAULT_FETCH_SIZE; + + protected static boolean ourLastResultsFromTranslationCache; // For testing. + protected static boolean ourLastResultsFromTranslationWithReverseCache; // For testing. + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + + @Autowired + protected FhirContext myContext; + + @Autowired + protected MemoryCacheService myMemoryCacheService; + + @Autowired + protected IIdHelperService myIdHelperService; + + @Autowired + protected ITermConceptMapDao myConceptMapDao; + + @Override + @Transactional(propagation = Propagation.REQUIRED) + public TranslateConceptResults translate(TranslationRequest theTranslationRequest) { + TranslateConceptResults retVal = new TranslateConceptResults(); + + CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder(); + CriteriaQuery query = + criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class); + Root root = query.from(TermConceptMapGroupElementTarget.class); + + Join elementJoin = + root.join("myConceptMapGroupElement"); + Join groupJoin = elementJoin.join("myConceptMapGroup"); + Join conceptMapJoin = groupJoin.join("myConceptMap"); + + List translationQueries = theTranslationRequest.getTranslationQueries(); + List cachedTargets; + ArrayList predicates; + Coding coding; + + // -- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version + String latestConceptMapVersion = null; + if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion()) + latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest); + + for (TranslationQuery translationQuery : translationQueries) { + cachedTargets = myMemoryCacheService.getIfPresent( + MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery); + if (cachedTargets == null) { + final List targets = new ArrayList<>(); + + predicates = new ArrayList<>(); + + coding = translationQuery.getCoding(); + if (coding.hasCode()) { + predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode())); + } else { + throw new InvalidRequestException( + Msg.code(842) + "A code must be provided for translation to occur."); + } + + if (coding.hasSystem()) { + predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem())); + } + + if (coding.hasVersion()) { + predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion())); + } + + if (translationQuery.hasTargetSystem()) { + predicates.add( + criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem())); + } + + if (translationQuery.hasUrl()) { + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl())); + if (translationQuery.hasConceptMapVersion()) { + // both url and conceptMapVersion + predicates.add(criteriaBuilder.equal( + conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion())); + } else { + if (StringUtils.isNotBlank(latestConceptMapVersion)) { + // only url and use latestConceptMapVersion + predicates.add( + criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion)); + } else { + predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion"))); + } + } + } + + if (translationQuery.hasSource()) { + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource())); + } + + if (translationQuery.hasTarget()) { + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget())); + } + + if (translationQuery.hasResourceId()) { + IIdType resourceId = translationQuery.getResourceId(); + JpaPid resourcePid = + myIdHelperService.getPidOrThrowException(RequestPartitionId.defaultPartition(), resourceId); + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), resourcePid.getId())); + } + + Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0])); + query.where(outerPredicate); + + // Use scrollable results. + final TypedQuery typedQuery = + myEntityManager.createQuery(query.select(root)); + org.hibernate.query.Query hibernateQuery = + (org.hibernate.query.Query) typedQuery; + hibernateQuery.setFetchSize(myFetchSize); + ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); + try (ScrollableResultsIterator scrollableResultsIterator = + new ScrollableResultsIterator<>(scrollableResults)) { + + Set matches = new HashSet<>(); + while (scrollableResultsIterator.hasNext()) { + TermConceptMapGroupElementTarget next = scrollableResultsIterator.next(); + if (matches.add(next)) { + + TranslateConceptResult translationMatch = new TranslateConceptResult(); + if (next.getEquivalence() != null) { + translationMatch.setEquivalence( + next.getEquivalence().toCode()); + } + + translationMatch.setCode(next.getCode()); + translationMatch.setSystem(next.getSystem()); + translationMatch.setSystemVersion(next.getSystemVersion()); + translationMatch.setDisplay(next.getDisplay()); + translationMatch.setValueSet(next.getValueSet()); + translationMatch.setSystemVersion(next.getSystemVersion()); + translationMatch.setConceptMapUrl(next.getConceptMapUrl()); + + targets.add(translationMatch); + } + } + } + + ourLastResultsFromTranslationCache = false; // For testing. + myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery, targets); + retVal.getResults().addAll(targets); + } else { + ourLastResultsFromTranslationCache = true; // For testing. + retVal.getResults().addAll(cachedTargets); + } + } + + buildTranslationResult(retVal); + return retVal; + } + + @Override + @Transactional(propagation = Propagation.REQUIRED) + public TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest) { + TranslateConceptResults retVal = new TranslateConceptResults(); + + CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder(); + CriteriaQuery query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class); + Root root = query.from(TermConceptMapGroupElement.class); + + Join targetJoin = + root.join("myConceptMapGroupElementTargets"); + Join groupJoin = root.join("myConceptMapGroup"); + Join conceptMapJoin = groupJoin.join("myConceptMap"); + + List translationQueries = theTranslationRequest.getTranslationQueries(); + List cachedElements; + ArrayList predicates; + Coding coding; + + // -- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version + String latestConceptMapVersion = null; + if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion()) + latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest); + + for (TranslationQuery translationQuery : translationQueries) { + cachedElements = myMemoryCacheService.getIfPresent( + MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery); + if (cachedElements == null) { + final List elements = new ArrayList<>(); + + predicates = new ArrayList<>(); + + coding = translationQuery.getCoding(); + String targetCode; + String targetCodeSystem = null; + if (coding.hasCode()) { + predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode())); + targetCode = coding.getCode(); + } else { + throw new InvalidRequestException( + Msg.code(843) + "A code must be provided for translation to occur."); + } + + if (coding.hasSystem()) { + predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem())); + targetCodeSystem = coding.getSystem(); + } + + if (coding.hasVersion()) { + predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion())); + } + + if (translationQuery.hasUrl()) { + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl())); + if (translationQuery.hasConceptMapVersion()) { + // both url and conceptMapVersion + predicates.add(criteriaBuilder.equal( + conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion())); + } else { + if (StringUtils.isNotBlank(latestConceptMapVersion)) { + // only url and use latestConceptMapVersion + predicates.add( + criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion)); + } else { + predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion"))); + } + } + } + + if (translationQuery.hasTargetSystem()) { + predicates.add( + criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem())); + } + + if (translationQuery.hasSource()) { + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource())); + } + + if (translationQuery.hasTarget()) { + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget())); + } + + if (translationQuery.hasResourceId()) { + IIdType resourceId = translationQuery.getResourceId(); + JpaPid resourcePid = + myIdHelperService.getPidOrThrowException(RequestPartitionId.defaultPartition(), resourceId); + predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), resourcePid.getId())); + } + + Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0])); + query.where(outerPredicate); + + // Use scrollable results. + final TypedQuery typedQuery = + myEntityManager.createQuery(query.select(root)); + org.hibernate.query.Query hibernateQuery = + (org.hibernate.query.Query) typedQuery; + hibernateQuery.setFetchSize(myFetchSize); + ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); + try (ScrollableResultsIterator scrollableResultsIterator = + new ScrollableResultsIterator<>(scrollableResults)) { + + Set matches = new HashSet<>(); + while (scrollableResultsIterator.hasNext()) { + TermConceptMapGroupElement nextElement = scrollableResultsIterator.next(); + + /* TODO: The invocation of the size() below does not seem to be necessary but for some reason, + * but removing it causes tests in TerminologySvcImplR4Test to fail. We use the outcome + * in a trace log to avoid ErrorProne flagging an unused return value. + */ + int size = + nextElement.getConceptMapGroupElementTargets().size(); + ourLog.trace("Have {} targets", size); + + myEntityManager.detach(nextElement); + + if (isNotBlank(targetCode)) { + for (TermConceptMapGroupElementTarget next : + nextElement.getConceptMapGroupElementTargets()) { + if (matches.add(next)) { + if (isBlank(targetCodeSystem) + || StringUtils.equals(targetCodeSystem, next.getSystem())) { + if (StringUtils.equals(targetCode, next.getCode())) { + TranslateConceptResult translationMatch = new TranslateConceptResult(); + translationMatch.setCode(nextElement.getCode()); + translationMatch.setSystem(nextElement.getSystem()); + translationMatch.setSystemVersion(nextElement.getSystemVersion()); + translationMatch.setDisplay(nextElement.getDisplay()); + translationMatch.setValueSet(nextElement.getValueSet()); + translationMatch.setSystemVersion(nextElement.getSystemVersion()); + translationMatch.setConceptMapUrl(nextElement.getConceptMapUrl()); + if (next.getEquivalence() != null) { + translationMatch.setEquivalence( + next.getEquivalence().toCode()); + } + + if (alreadyContainsMapping(elements, translationMatch) + || alreadyContainsMapping(retVal.getResults(), translationMatch)) { + continue; + } + + elements.add(translationMatch); + } + } + } + } + } + } + } + + ourLastResultsFromTranslationWithReverseCache = false; // For testing. + myMemoryCacheService.put( + MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery, elements); + retVal.getResults().addAll(elements); + } else { + ourLastResultsFromTranslationWithReverseCache = true; // For testing. + retVal.getResults().addAll(cachedElements); + } + } + + buildTranslationResult(retVal); + return retVal; + } + + @Override + public FhirContext getFhirContext() { + return myContext; + } + + // Special case for the translate operation with url and without + // conceptMapVersion, find the latest conecptMapVersion + private String getLatestConceptMapVersion(TranslationRequest theTranslationRequest) { + + Pageable page = PageRequest.of(0, 1); + List theConceptMapList = myConceptMapDao.getTermConceptMapEntitiesByUrlOrderByMostRecentUpdate( + page, theTranslationRequest.getUrl()); + if (!theConceptMapList.isEmpty()) { + return theConceptMapList.get(0).getVersion(); + } + + return null; + } + + private void buildTranslationResult(TranslateConceptResults theTranslationResult) { + + String msg; + if (theTranslationResult.getResults().isEmpty()) { + theTranslationResult.setResult(false); + msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "noMatchesFound"); + theTranslationResult.setMessage(msg); + } else { + theTranslationResult.setResult(true); + msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "matchesFound"); + theTranslationResult.setMessage(msg); + } + } + + private boolean alreadyContainsMapping( + List elements, TranslateConceptResult translationMatch) { + for (TranslateConceptResult nextExistingElement : elements) { + if (StringUtils.equals(nextExistingElement.getSystem(), translationMatch.getSystem())) { + if (StringUtils.equals(nextExistingElement.getSystemVersion(), translationMatch.getSystemVersion())) { + if (StringUtils.equals(nextExistingElement.getCode(), translationMatch.getCode())) { + return true; + } + } + } + } + return false; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptMappingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptMappingSvcImpl.java index d4c7928b62a..781f5e281ac 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptMappingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermConceptMappingSvcImpl.java @@ -19,15 +19,10 @@ */ package ca.uhn.fhir.jpa.term; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.TranslateConceptResult; import ca.uhn.fhir.context.support.TranslateConceptResults; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.jpa.api.model.TranslationQuery; import ca.uhn.fhir.jpa.api.model.TranslationRequest; -import ca.uhn.fhir.jpa.api.svc.IIdHelperService; -import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao; @@ -35,21 +30,13 @@ import ca.uhn.fhir.jpa.entity.TermConceptMap; import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; -import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; -import ca.uhn.fhir.jpa.util.MemoryCacheService; -import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.ValidateUtil; import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.lang3.StringUtils; -import org.hibernate.ScrollMode; -import org.hibernate.ScrollableResults; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Coding; @@ -61,39 +48,17 @@ import org.hl7.fhir.r4.model.UriType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; import java.util.Optional; -import java.util.Set; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.PersistenceContextType; -import javax.persistence.TypedQuery; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; import static ca.uhn.fhir.jpa.term.TermReadSvcImpl.isPlaceholder; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc { +public class TermConceptMappingSvcImpl extends TermConceptClientMappingSvcImpl implements ITermConceptMappingSvc { private static final Logger ourLog = LoggerFactory.getLogger(TermConceptMappingSvcImpl.class); - private static boolean ourLastResultsFromTranslationCache; // For testing. - private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing. - private final int myFetchSize = TermReadSvcImpl.DEFAULT_FETCH_SIZE; - - @Autowired - protected ITermConceptMapDao myConceptMapDao; @Autowired protected ITermConceptMapGroupDao myConceptMapGroupDao; @@ -104,29 +69,12 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc { @Autowired protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao; - @PersistenceContext(type = PersistenceContextType.TRANSACTION) - protected EntityManager myEntityManager; - - @Autowired - private FhirContext myContext; - - @Autowired - private MemoryCacheService myMemoryCacheService; - - @Autowired - private IIdHelperService myIdHelperService; - @Override @Transactional public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { deleteConceptMap(theResourceTable); } - @Override - public FhirContext getFhirContext() { - return myContext; - } - @Override @Transactional public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { @@ -205,7 +153,7 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc { optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion); } - if (!optionalExistingTermConceptMapByUrl.isPresent()) { + if (optionalExistingTermConceptMapByUrl.isEmpty()) { try { if (isNotBlank(source)) { termConceptMap.setSource(source); @@ -328,320 +276,6 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc { theConceptMap.getIdElement().toVersionless().getValueAsString()); } - @Override - @Transactional(propagation = Propagation.REQUIRED) - public TranslateConceptResults translate(TranslationRequest theTranslationRequest) { - TranslateConceptResults retVal = new TranslateConceptResults(); - - CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder(); - CriteriaQuery query = - criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class); - Root root = query.from(TermConceptMapGroupElementTarget.class); - - Join elementJoin = - root.join("myConceptMapGroupElement"); - Join groupJoin = elementJoin.join("myConceptMapGroup"); - Join conceptMapJoin = groupJoin.join("myConceptMap"); - - List translationQueries = theTranslationRequest.getTranslationQueries(); - List cachedTargets; - ArrayList predicates; - Coding coding; - - // -- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version - String latestConceptMapVersion = null; - if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion()) - latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest); - - for (TranslationQuery translationQuery : translationQueries) { - cachedTargets = myMemoryCacheService.getIfPresent( - MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery); - if (cachedTargets == null) { - final List targets = new ArrayList<>(); - - predicates = new ArrayList<>(); - - coding = translationQuery.getCoding(); - if (coding.hasCode()) { - predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode())); - } else { - throw new InvalidRequestException( - Msg.code(842) + "A code must be provided for translation to occur."); - } - - if (coding.hasSystem()) { - predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem())); - } - - if (coding.hasVersion()) { - predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion())); - } - - if (translationQuery.hasTargetSystem()) { - predicates.add( - criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem())); - } - - if (translationQuery.hasUrl()) { - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl())); - if (translationQuery.hasConceptMapVersion()) { - // both url and conceptMapVersion - predicates.add(criteriaBuilder.equal( - conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion())); - } else { - if (StringUtils.isNotBlank(latestConceptMapVersion)) { - // only url and use latestConceptMapVersion - predicates.add( - criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion)); - } else { - predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion"))); - } - } - } - - if (translationQuery.hasSource()) { - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource())); - } - - if (translationQuery.hasTarget()) { - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget())); - } - - if (translationQuery.hasResourceId()) { - IIdType resourceId = translationQuery.getResourceId(); - JpaPid resourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.defaultPartition(), resourceId); - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), resourcePid.getId())); - } - - Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0])); - query.where(outerPredicate); - - // Use scrollable results. - final TypedQuery typedQuery = - myEntityManager.createQuery(query.select(root)); - org.hibernate.query.Query hibernateQuery = - (org.hibernate.query.Query) typedQuery; - hibernateQuery.setFetchSize(myFetchSize); - ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); - try (ScrollableResultsIterator scrollableResultsIterator = - new ScrollableResultsIterator<>(scrollableResults)) { - - Set matches = new HashSet<>(); - while (scrollableResultsIterator.hasNext()) { - TermConceptMapGroupElementTarget next = scrollableResultsIterator.next(); - if (matches.add(next)) { - - TranslateConceptResult translationMatch = new TranslateConceptResult(); - if (next.getEquivalence() != null) { - translationMatch.setEquivalence( - next.getEquivalence().toCode()); - } - - translationMatch.setCode(next.getCode()); - translationMatch.setSystem(next.getSystem()); - translationMatch.setSystemVersion(next.getSystemVersion()); - translationMatch.setDisplay(next.getDisplay()); - translationMatch.setValueSet(next.getValueSet()); - translationMatch.setSystemVersion(next.getSystemVersion()); - translationMatch.setConceptMapUrl(next.getConceptMapUrl()); - - targets.add(translationMatch); - } - } - } - - ourLastResultsFromTranslationCache = false; // For testing. - myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery, targets); - retVal.getResults().addAll(targets); - } else { - ourLastResultsFromTranslationCache = true; // For testing. - retVal.getResults().addAll(cachedTargets); - } - } - - buildTranslationResult(retVal); - return retVal; - } - - @Override - @Transactional(propagation = Propagation.REQUIRED) - public TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest) { - TranslateConceptResults retVal = new TranslateConceptResults(); - - CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder(); - CriteriaQuery query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class); - Root root = query.from(TermConceptMapGroupElement.class); - - Join targetJoin = - root.join("myConceptMapGroupElementTargets"); - Join groupJoin = root.join("myConceptMapGroup"); - Join conceptMapJoin = groupJoin.join("myConceptMap"); - - List translationQueries = theTranslationRequest.getTranslationQueries(); - List cachedElements; - ArrayList predicates; - Coding coding; - - // -- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version - String latestConceptMapVersion = null; - if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion()) - latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest); - - for (TranslationQuery translationQuery : translationQueries) { - cachedElements = myMemoryCacheService.getIfPresent( - MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery); - if (cachedElements == null) { - final List elements = new ArrayList<>(); - - predicates = new ArrayList<>(); - - coding = translationQuery.getCoding(); - String targetCode; - String targetCodeSystem = null; - if (coding.hasCode()) { - predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode())); - targetCode = coding.getCode(); - } else { - throw new InvalidRequestException( - Msg.code(843) + "A code must be provided for translation to occur."); - } - - if (coding.hasSystem()) { - predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem())); - targetCodeSystem = coding.getSystem(); - } - - if (coding.hasVersion()) { - predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion())); - } - - if (translationQuery.hasUrl()) { - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl())); - if (translationQuery.hasConceptMapVersion()) { - // both url and conceptMapVersion - predicates.add(criteriaBuilder.equal( - conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion())); - } else { - if (StringUtils.isNotBlank(latestConceptMapVersion)) { - // only url and use latestConceptMapVersion - predicates.add( - criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion)); - } else { - predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion"))); - } - } - } - - if (translationQuery.hasTargetSystem()) { - predicates.add( - criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem())); - } - - if (translationQuery.hasSource()) { - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource())); - } - - if (translationQuery.hasTarget()) { - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget())); - } - - if (translationQuery.hasResourceId()) { - IIdType resourceId = translationQuery.getResourceId(); - JpaPid resourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.defaultPartition(), resourceId); - predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), resourcePid.getId())); - } - - Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0])); - query.where(outerPredicate); - - // Use scrollable results. - final TypedQuery typedQuery = - myEntityManager.createQuery(query.select(root)); - org.hibernate.query.Query hibernateQuery = - (org.hibernate.query.Query) typedQuery; - hibernateQuery.setFetchSize(myFetchSize); - ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); - try (ScrollableResultsIterator scrollableResultsIterator = - new ScrollableResultsIterator<>(scrollableResults)) { - - Set matches = new HashSet<>(); - while (scrollableResultsIterator.hasNext()) { - TermConceptMapGroupElement nextElement = scrollableResultsIterator.next(); - - /* TODO: The invocation of the size() below does not seem to be necessary but for some reason, - * but removing it causes tests in TerminologySvcImplR4Test to fail. We use the outcome - * in a trace log to avoid ErrorProne flagging an unused return value. - */ - int size = - nextElement.getConceptMapGroupElementTargets().size(); - ourLog.trace("Have {} targets", size); - - myEntityManager.detach(nextElement); - - if (isNotBlank(targetCode)) { - for (TermConceptMapGroupElementTarget next : - nextElement.getConceptMapGroupElementTargets()) { - if (matches.add(next)) { - if (isBlank(targetCodeSystem) - || StringUtils.equals(targetCodeSystem, next.getSystem())) { - if (StringUtils.equals(targetCode, next.getCode())) { - TranslateConceptResult translationMatch = new TranslateConceptResult(); - translationMatch.setCode(nextElement.getCode()); - translationMatch.setSystem(nextElement.getSystem()); - translationMatch.setSystemVersion(nextElement.getSystemVersion()); - translationMatch.setDisplay(nextElement.getDisplay()); - translationMatch.setValueSet(nextElement.getValueSet()); - translationMatch.setSystemVersion(nextElement.getSystemVersion()); - translationMatch.setConceptMapUrl(nextElement.getConceptMapUrl()); - if (next.getEquivalence() != null) { - translationMatch.setEquivalence( - next.getEquivalence().toCode()); - } - - if (alreadyContainsMapping(elements, translationMatch) - || alreadyContainsMapping(retVal.getResults(), translationMatch)) { - continue; - } - - elements.add(translationMatch); - } - } - } - } - } - } - } - - ourLastResultsFromTranslationWithReverseCache = false; // For testing. - myMemoryCacheService.put( - MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery, elements); - retVal.getResults().addAll(elements); - } else { - ourLastResultsFromTranslationWithReverseCache = true; // For testing. - retVal.getResults().addAll(cachedElements); - } - } - - buildTranslationResult(retVal); - return retVal; - } - - private boolean alreadyContainsMapping( - List elements, TranslateConceptResult translationMatch) { - for (TranslateConceptResult nextExistingElement : elements) { - if (StringUtils.equals(nextExistingElement.getSystem(), translationMatch.getSystem())) { - if (StringUtils.equals(nextExistingElement.getSystemVersion(), translationMatch.getSystemVersion())) { - if (StringUtils.equals(nextExistingElement.getCode(), translationMatch.getCode())) { - return true; - } - } - } - } - return false; - } - public void deleteConceptMap(ResourceTable theResourceTable) { // Get existing entity so it can be deleted. Optional optionalExistingTermConceptMapById = @@ -671,34 +305,6 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc { } } - // Special case for the translate operation with url and without - // conceptMapVersion, find the latest conecptMapVersion - private String getLatestConceptMapVersion(TranslationRequest theTranslationRequest) { - - Pageable page = PageRequest.of(0, 1); - List theConceptMapList = myConceptMapDao.getTermConceptMapEntitiesByUrlOrderByMostRecentUpdate( - page, theTranslationRequest.getUrl()); - if (!theConceptMapList.isEmpty()) { - return theConceptMapList.get(0).getVersion(); - } - - return null; - } - - private void buildTranslationResult(TranslateConceptResults theTranslationResult) { - - String msg; - if (theTranslationResult.getResults().isEmpty()) { - theTranslationResult.setResult(false); - msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "noMatchesFound"); - theTranslationResult.setMessage(msg); - } else { - theTranslationResult.setResult(true); - msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "matchesFound"); - theTranslationResult.setMessage(msg); - } - } - /** * This method is present only for unit tests, do not call from client code */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptClientMappingSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptClientMappingSvc.java new file mode 100644 index 00000000000..a07e3a229b7 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptClientMappingSvc.java @@ -0,0 +1,34 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.term.api; + +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.TranslateConceptResults; +import ca.uhn.fhir.jpa.api.model.TranslationRequest; + +/** + * Represents the terminology translate functions + */ +public interface ITermConceptClientMappingSvc extends IValidationSupport { + + TranslateConceptResults translate(TranslationRequest theTranslationRequest); + + TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptMappingSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptMappingSvc.java index 127861a1961..1fc1b494693 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptMappingSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermConceptMappingSvc.java @@ -19,17 +19,13 @@ */ package ca.uhn.fhir.jpa.term.api; -import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.context.support.TranslateConceptResults; -import ca.uhn.fhir.jpa.api.model.TranslationRequest; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import org.hl7.fhir.r4.model.ConceptMap; -public interface ITermConceptMappingSvc extends IValidationSupport { - - TranslateConceptResults translate(TranslationRequest theTranslationRequest); - - TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest); +/** + * Represents ConceptMap translation and persistence operations + */ +public interface ITermConceptMappingSvc extends ITermConceptClientMappingSvc { void deleteConceptMapAndChildren(ResourceTable theResourceTable);