Add ITermConceptClientMappingSvc interface to represent terminology t… (#5410)

* Add ITermConceptClientMappingSvc interface to represent terminology translate functions

* Add javadoc

---------

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2023-10-31 13:21:35 -04:00 committed by GitHub
parent a7a446903f
commit c89fc46863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 460 additions and 404 deletions

View File

@ -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<JpaPid> myIdHelperService;
@Autowired
protected ITermConceptMapDao myConceptMapDao;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public TranslateConceptResults translate(TranslationRequest theTranslationRequest) {
TranslateConceptResults retVal = new TranslateConceptResults();
CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<TermConceptMapGroupElementTarget> query =
criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class);
Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class);
Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin =
root.join("myConceptMapGroupElement");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TranslateConceptResult> cachedTargets;
ArrayList<Predicate> 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<TranslateConceptResult> 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<TermConceptMapGroupElementTarget> typedQuery =
myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery =
(org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElementTarget> scrollableResultsIterator =
new ScrollableResultsIterator<>(scrollableResults)) {
Set<TermConceptMapGroupElementTarget> 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<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class);
Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class);
Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin =
root.join("myConceptMapGroupElementTargets");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TranslateConceptResult> cachedElements;
ArrayList<Predicate> 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<TranslateConceptResult> 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<TermConceptMapGroupElement> typedQuery =
myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery =
(org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElement> scrollableResultsIterator =
new ScrollableResultsIterator<>(scrollableResults)) {
Set<TermConceptMapGroupElementTarget> 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<TermConceptMap> 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<TranslateConceptResult> 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;
}
}

View File

@ -19,15 +19,10 @@
*/ */
package ca.uhn.fhir.jpa.term; 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.TranslateConceptResult;
import ca.uhn.fhir.context.support.TranslateConceptResults; import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.i18n.Msg; 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.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.ITermConceptMapGroupDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao; 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.TermConceptMapGroup;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; 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.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; 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.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import com.google.common.annotations.VisibleForTesting; 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.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; 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 org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional; 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 ca.uhn.fhir.jpa.term.TermReadSvcImpl.isPlaceholder;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; 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 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 @Autowired
protected ITermConceptMapGroupDao myConceptMapGroupDao; protected ITermConceptMapGroupDao myConceptMapGroupDao;
@ -104,29 +69,12 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc {
@Autowired @Autowired
protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao; protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private FhirContext myContext;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Autowired
private IIdHelperService<JpaPid> myIdHelperService;
@Override @Override
@Transactional @Transactional
public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { public void deleteConceptMapAndChildren(ResourceTable theResourceTable) {
deleteConceptMap(theResourceTable); deleteConceptMap(theResourceTable);
} }
@Override
public FhirContext getFhirContext() {
return myContext;
}
@Override @Override
@Transactional @Transactional
public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
@ -205,7 +153,7 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc {
optionalExistingTermConceptMapByUrl = optionalExistingTermConceptMapByUrl =
myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion); myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion);
} }
if (!optionalExistingTermConceptMapByUrl.isPresent()) { if (optionalExistingTermConceptMapByUrl.isEmpty()) {
try { try {
if (isNotBlank(source)) { if (isNotBlank(source)) {
termConceptMap.setSource(source); termConceptMap.setSource(source);
@ -328,320 +276,6 @@ public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc {
theConceptMap.getIdElement().toVersionless().getValueAsString()); theConceptMap.getIdElement().toVersionless().getValueAsString());
} }
@Override
@Transactional(propagation = Propagation.REQUIRED)
public TranslateConceptResults translate(TranslationRequest theTranslationRequest) {
TranslateConceptResults retVal = new TranslateConceptResults();
CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<TermConceptMapGroupElementTarget> query =
criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class);
Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class);
Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin =
root.join("myConceptMapGroupElement");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TranslateConceptResult> cachedTargets;
ArrayList<Predicate> 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<TranslateConceptResult> 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<TermConceptMapGroupElementTarget> typedQuery =
myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery =
(org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElementTarget> scrollableResultsIterator =
new ScrollableResultsIterator<>(scrollableResults)) {
Set<TermConceptMapGroupElementTarget> 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<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class);
Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class);
Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin =
root.join("myConceptMapGroupElementTargets");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TranslateConceptResult> cachedElements;
ArrayList<Predicate> 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<TranslateConceptResult> 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<TermConceptMapGroupElement> typedQuery =
myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery =
(org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElement> scrollableResultsIterator =
new ScrollableResultsIterator<>(scrollableResults)) {
Set<TermConceptMapGroupElementTarget> 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<TranslateConceptResult> 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) { public void deleteConceptMap(ResourceTable theResourceTable) {
// Get existing entity so it can be deleted. // Get existing entity so it can be deleted.
Optional<TermConceptMap> optionalExistingTermConceptMapById = Optional<TermConceptMap> 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<TermConceptMap> 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 * This method is present only for unit tests, do not call from client code
*/ */

View File

@ -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);
}

View File

@ -19,17 +19,13 @@
*/ */
package ca.uhn.fhir.jpa.term.api; 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 ca.uhn.fhir.jpa.model.entity.ResourceTable;
import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.ConceptMap;
public interface ITermConceptMappingSvc extends IValidationSupport { /**
* Represents ConceptMap translation and persistence operations
TranslateConceptResults translate(TranslationRequest theTranslationRequest); */
public interface ITermConceptMappingSvc extends ITermConceptClientMappingSvc {
TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest);
void deleteConceptMapAndChildren(ResourceTable theResourceTable); void deleteConceptMapAndChildren(ResourceTable theResourceTable);